refactor(storybook): simplify component and preview examples
Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
This commit is contained in:
1 parent
48977e7823
commit
4c29894e56
81 files changed
+520
-671
No files matched your search
@@ -73,7 +73,7 @@ import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
|
||||
## 7. Storybook
|
||||
|
||||
`Center Graphic` 그룹(기본 sidebar): `Overview`, `Title`, `Information`, `Spatial`, `Facility`(터널 · 교량 · 교차로 · 분기점 · 나들목 · 휴게소·졸음쉼터 · 연계 시설물), verification 숨김: Title `Playground`, Facility `직접 테스트 (터널)`. 제작 대상 해상도는 주소 `?centerResolution=3840x2160`으로 지정한다.
|
||||
`Center Graphic` 그룹(기본 sidebar): `Title`, `Information`, `Spatial`, `Facility`(터널 · 교량 · 교차로 · 분기점 · 나들목 · 휴게소·졸음쉼터 · 연계 시설물), verification 숨김: `Overview`(배율 규칙·상태 표, 직접 URL `center-graphic-overview--overview` 유지), Title `Playground`, Facility `직접 테스트 (터널)`. 제작 대상 해상도는 주소 `?centerResolution=3840x2160`으로 지정한다.
|
||||
|
||||
## 8. 미확정 / 보류 (원본 기준)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ pointer drag/swipe와 snap, loop·align·dragFree·skipSnaps 등 options, plugin
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Carousel"): Basic(한 개 + `setApi`로 `slide n / 5` 표시), Multiple items(`basis-1/2`), Loop(3개), Vertical(세로), Media(AspectRatio 조합)를 한 페이지에 둔다. Card와 AspectRatio는 Story 조합이며 Carousel은 둘을 import하지 않는다.
|
||||
- 대표 Story `All`("Carousel"): Basic(한 개, `setApi` 사용. `slide n / 5` 표시는 숨김 `Basic`에만 둔다), Multiple items(`basis-1/2`), Loop(3개), Vertical(세로), Media(AspectRatio 조합)를 한 페이지에 둔다. Card와 AspectRatio는 Story 조합이며 Carousel은 둘을 import하지 않는다.
|
||||
- verification: `Basic`, `Multiple Items`, `Loop`, `Vertical`.
|
||||
|
||||
## 10. 소비자 확장성
|
||||
|
||||
@@ -100,7 +100,7 @@ Radix 구조·ARIA(`role=checkbox`, `aria-checked`)·키보드, `data-slot`, `cn
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Checkbox"): `unchecked`, `checked`, `indeterminate`, `interactive`(controlled), `hover`, `focus`, `error`, `disabled`, `long`. Focus는 `play`로 실제 focus.
|
||||
- 대표 Story `All`("Checkbox"): `unchecked`, `checked`, `indeterminate`, `hover`, `focus`, `error`, `disabled`, `long`. Focus는 `play`로 실제 focus. `indeterminate`는 controlled 예시를 겸해 클릭하면 checked가 된다(Radix, 2026-10-22 별도 `interactive` 구역 통합). `hover`는 Unchecked·Checked를 Storybook 전용 `data-sb-hover`(`.storybook/pseudo-states.ts`)로 고정해 보여 준다. Checkbox API가 아니며 소비 코드에 붙이지 않는다.
|
||||
- verification: `Unchecked`, `Checked`, `Indeterminate`, `Disabled`, `Error`, `Playground`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
@@ -102,7 +102,7 @@ Popover: Portal, collision, Escape, 바깥 클릭 닫힘, trigger focus 복귀.
|
||||
|
||||
## 10. Storybook
|
||||
|
||||
- 대표 Story `All`("Combobox"): Empty placeholder + search(Remix는 disabled option), Selected(controlled, 값 표시), Medium, Long option, Disabled를 한 페이지에 둔다. Label은 Story 조합이다.
|
||||
- 대표 Story `All`("Combobox"): Overview(Placeholder + search, Remix는 disabled option), Sizes(Medium, 같은 size의 Select / Combobox 비교), States(Selected(controlled), Disabled), Usage(Long option). Label은 Story 조합이다. 대표 화면은 선택값 표시(`value = …`)를 숨기며 숨김 `Controlled`는 유지한다.
|
||||
- verification: `Empty`, `Controlled`, `Long Option`.
|
||||
|
||||
## 11. 사용자 확정 결정 / 보류
|
||||
|
||||
@@ -69,7 +69,7 @@ Component Mapping 위치: `packages/ui/src/theme/command.css` (`--hm-command-*`)
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Command"): Search + disabled item(선택 로그), Groups·icons·shortcut(`CalendarIcon`, `UserIcon`, `SettingsIcon`), Long list(30개, List가 scroll)를 한 페이지에 둔다. 테두리 박스는 Story 조합이다.
|
||||
- 대표 Story `All`("Command"): Search + disabled item(선택 로그는 숨김 `Search`에만 표시), Groups·icons·shortcut(`CalendarIcon`, `UserIcon`, `SettingsIcon`), Long list(30개, List가 scroll)를 한 페이지에 둔다. 테두리 박스는 Story 조합이다.
|
||||
- verification: `Search`, `Grouped`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
@@ -90,7 +90,7 @@ Popover: Portal, collision 처리, Escape, 바깥 클릭 닫힘, trigger focus
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Date Picker"): Empty, Selected(`2026.10.06`), 기본 형식(`2026.01.09`)/custom override, Controlled(값 표시), Medium, Disabled dates(5일 이전·일요일), Disabled를 한 페이지에 둔다. Label은 Story 조합이며 `today`는 Calendar prop으로 2026-10-02에 고정.
|
||||
- 대표 Story `All`("Date Picker"): Empty, Selected(`2026.10.06`), 기본 형식(`2026.01.09`)/custom override, Controlled(대표 화면은 값 표시 숨김, 숨김 `Controlled`는 표시), Medium, Disabled dates(5일 이전·일요일), Disabled를 한 페이지에 둔다. Label은 Story 조합이며 `today`는 Calendar prop으로 2026-10-02에 고정.
|
||||
- verification: `Empty`, `Selected`, `Controlled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
@@ -84,7 +84,7 @@ native `<input>` 동작 전체(`type`, `file:` 스타일 class, `placeholder`),
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Input"): 구역 `sizes`, `default`, `focus`, `error`, `error-message`, `disabled`, `readonly`. Focus는 `play` 함수로 실제 focus를 준다.
|
||||
- 대표 Story `All`("Input"): Sizes(Small / Medium / Large 비교) → States(`focus`, `disabled`, `readonly`) → Validation(`error`, `error-message`). Focus는 `play` 함수로 실제 focus를 준다. 2026-10-22 Overview(`default`, size 생략 = Large)는 Sizes의 Large와 같아 대표 화면에서 뺐고, 크기별 수치 목록·설명은 숨김 `Sizes`("Compare Sizes")에 남겼다. size 생략 시 Large(§5)는 그대로다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Disabled`, `Readonly`, `Sizes`("Compare Sizes"), `Small`, `Medium`, `Large`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Input 컴포넌트의 일부가 아니다.
|
||||
- sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조.
|
||||
|
||||
@@ -57,7 +57,7 @@ shadcn `size-4 border-input text-primary shadow-xs dark:` class 제거 → theme
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `vertical`, `horizontal`, `hover`, `focus`, `error`, `disabled-item`, `disabled-group`. Focus는 `play`로 실제 focus. verification: `Vertical`, `Horizontal`, `Disabled`, `Error`.
|
||||
대표 Story `All`: `vertical`, `horizontal`, `hover`, `focus`, `error`, `disabled-item`, `disabled-group`. Focus는 `play`로 실제 focus. `hover`는 첫 항목을 Storybook 전용 `data-sb-hover`로 고정해 보여 주며 RadioGroupItem API가 아니다. verification: `Vertical`, `Horizontal`, `Disabled`, `Error`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ Radix Root/Trigger/Portal/Content/Viewport/Item/ItemText 구조, 키보드 조
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Select"): 구역 `default`(Large, size 생략), `selected`, `disabled`, `open`.
|
||||
- 대표 Story `All`("Select"): Sizes(Medium / Large 비교, 각 Trigger로 실제 목록 열기) → States(`selected`, `disabled`, `disabled-placeholder`) → Invalid / Focus(`invalid`, `invalid-disabled`). 2026-10-22 Overview의 `default`·로드 시 열린 `open` 예시는 대표 화면에서 뺐다. 열린 목록은 숨김 `Open`, 크기별 수치 목록은 숨김 `Sizes`("Compare Sizes")에서 확인한다. size 생략 시 Large(§5)는 그대로다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Open`, `Selected`, `Disabled`, `Sizes`("Compare Sizes"), `Medium`, `Large`, `LongContent`, `Scroll`, `Playground`(HM 전용).
|
||||
- `Disabled` Story 구성: Trigger 비활성 상태들(placeholder 포함).
|
||||
- `InvalidFocus` verification Story: 앞/뒤 버튼 사이의 Invalid Trigger와 Disabled Invalid Trigger. Tab / Shift+Tab, Enter / Space, Escape 및 focus return을 실제 키보드로 확인한다.
|
||||
|
||||
@@ -56,7 +56,7 @@ shadcn의 `h-[1.15rem] w-8`, `border`, `shadow-xs`, `dark:` class 제거 → the
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `off`, `on`, `interactive`(controlled), `hover`, `focus`, `disabled`. Focus는 `play`로 실제 focus. verification: `Off`, `On`, `Disabled`.
|
||||
대표 Story `All`: `off`, `on`, `hover`, `focus`, `disabled`(2026-10-22 `interactive` 구역 제거, 모든 예시가 실제 클릭으로 토글). `hover`는 Off·On을 Storybook 전용 `data-sb-hover`로 고정해 보여 주며 Switch API가 아니다. Focus는 `play`로 실제 focus. verification: `Off`, `On`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ container(`overflow-x-auto`)와 table 구조, `caption-bottom`, Head/Cell의 che
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Table"): Basic(Badge, 숫자 열, footer, caption), Selected row(Checkbox, local state), Long text, Horizontal overflow(1100px 표)를 한 페이지에 둔다.
|
||||
- 대표 Story `All`("Table"): Basic(Badge, 숫자 열, footer. 대표 화면은 보이는 caption 대신 `aria-label`로 표 이름을 주고, `TableCaption` 예시는 숨김 `Basic`에 남긴다), Selected row(Checkbox, local state), Long text, Horizontal overflow(1100px 표)를 한 페이지에 둔다.
|
||||
- verification: `Basic`, `Selected`, `Long Text`, `Horizontal Overflow`.
|
||||
- Checkbox·Badge는 Story 조합이다. Story의 폭 wrapper에는 `minWidth: 0`이 필요하다(grid item의 `min-width:auto`가 Table 최소 폭만큼 부모를 넓힘).
|
||||
|
||||
|
||||
@@ -126,12 +126,12 @@ Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving ta
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary, **Default API**는 `fill`만 명시한 Primary와 `fill` + `panelSurface="content"`를 명시한 Primary를 비교한다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다.
|
||||
- verification(`tags: ["verification"]`): `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다.
|
||||
- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary다. 같은 Primary가 반복되던 **Default API** 구역(`fill`만, `fill` + `panelSurface="content"` 명시)은 2026-10-22 숨김 verification `DefaultApi`로 옮겼다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다.
|
||||
- verification(`tags: ["verification"]`): `DefaultApi`, `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다.
|
||||
- `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다.
|
||||
- non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10).
|
||||
- 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다.
|
||||
- Overview / Default API / Variants / States 및 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`를 보존한다.
|
||||
- Overview / Variants / States, `DefaultApi` 및 Fill verification 예제의 콘텐츠 panel은 `px-0 py-6`로 통일해 D-01을 실제로 반영한다. Primary는 상위 content 면, Secondary는 상위 surface 면에 연결하며 내부 패널의 별도 border·배경·radius를 제거한다. Compatibility의 Legacy non-fill 두 예제만 이전 `border bg-card rounded-b p-4`를 보존한다.
|
||||
- **Usage**(2026-10-09 추가)는 공개 Card / Input / Textarea / Table을 조합해 Card 내부 Secondary와 페이지 content 면 위 Primary를 보여준다. 두 예제 모두 `TabsContent`는 `px-0 py-6`이며 별도 배경·border·radius를 추가하지 않는다. 대표 Story ID·기존 구역·verification Story는 유지한다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
@@ -86,7 +86,7 @@ native `<textarea>` 동작 전체, `data-slot`, `cn(..., className)`, `transitio
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Textarea"): 구역 `default`, `hover`, `focus`, `error`, `error-message`, `readonly`, `disabled`, `multiline`, `resize`. Focus는 `play` 함수로 실제 focus를 준다. Hover는 별도 pseudo 복제 없이 Default와 같다는 설명만 둔다.
|
||||
- 대표 Story `All`("Textarea"): 구역 `default`, `focus`, `error`, `error-message`, `readonly`, `disabled`, `multiline`, `resize`. Focus는 `play` 함수로 실제 focus를 준다. Hover는 Default와 같아(TEXTAREA-05) 2026-10-22 대표 화면의 중복 `hover` 예시를 뺐다. 실제 포인터 확인은 Default 예시에서 한다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Readonly`, `Disabled`, `LongContent`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Textarea 컴포넌트의 일부가 아니다.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Radix 구조·ARIA·roving focus(방향키, Home/End, disabled 건너뜀, `loop`
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Toggle Group"): Single(local state, 값 표시), Multiple(icon-only, local state), Disabled group / item, Vertical(icon-only), `spacing={2}`, Many items(`spacing={1}` + `flex-wrap`)을 한 페이지에 둔다.
|
||||
- 대표 Story `All`("Toggle Group"): Single(local state), Multiple(icon-only, local state), Disabled group / item, Vertical(icon-only), `spacing={2}`, Many items(`spacing={1}` + `flex-wrap`)을 한 페이지에 둔다. 대표 화면은 예시 제목·선택값 표시(`value = …`)를 생략하고 구역 제목으로 구분한다. 값 표시는 verification에 남긴다.
|
||||
- verification: `Single`, `Multiple`, `Vertical`.
|
||||
- meta에 `component`를 지정하지 않는다(props가 `single | multiple` union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Radix 구조·`aria-pressed`·`data-state`·Enter/Space·controlled/uncontrolled
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Toggle"): Off/On(default·outline), Icon-only와 icon + text(`BoldIcon`, `ItalicIcon`, `StarIcon`), Sizes, Disabled(off/on), Controlled(`pressed` 상태 표시), className 확장을 한 페이지에 둔다.
|
||||
- 대표 Story `All`("Toggle"): Overview(Off/On, default·outline), 단독 · 연결 그룹(단독 Toggle, `spacing=0` ToggleGroup 단일·다중 선택), Sizes, States(Disabled off/on). 2026-10-22 Icon-only·icon + text 예시는 숨김 `IconOnly`로만 확인하고, Controlled(`pressed` 표시)·className 확장 예시는 대표 화면에서 제거했다. `pressed` / `onPressedChange`와 `className` 확장 API(§5)는 그대로다.
|
||||
- verification: `States`, `Icon Only`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
@@ -87,7 +87,7 @@ Data Table은 기존 Table·Pagination·Checkbox·Select·Dropdown Menu 등을
|
||||
- LNB ONLY: 기존 독립 LNB 구조와 [LNB Mobile Header/Menu](../patterns/lnb.md#3b-lnb-mobile-header-2026-10-07)를 유지한다.
|
||||
- DASHBOARD ONLY: Sidebar를 생성하지 않는다. 대표 `patterns-layout-dashboard-layout--all`의 전용 Header(48px)·업무 상태 필터 + Content, 최소 폭 960px·내부 가로 스크롤을 따른다. `DashboardSidebar`와 `sidebar` slot은 호환성·검증용이며 과거 Sidebar verification Story는 신규 운영 화면 구현 기준이 아니다.
|
||||
- Dashboard Status: 공개 `@hm-design/ui/dashboard-status.css`는 변수만 제공한다. [Theme → Status CSS → 기존 Outline Badge mapping·React 소비 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)를 사용한다. `issue`는 독립 flag다. [Static HTML Reference](../patterns/dashboard-status-static.html)도 같은 계약을 소비한다.
|
||||
- FilterBar: [입력 draft / 조회 applied·초기화 예제](../patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 확인한다. 대표 `patterns-data-filter-bar--all`은 입력/조회 적용 상태와 초기화 callback을 구분하는 조합 예시이며 DataTable의 `patterns-data-data-table--with-filter-bar-story`는 즉시 적용 verification이다.
|
||||
- FilterBar: [입력 draft / 조회 applied·초기화 예제](../patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 확인한다. 대표 `patterns-data-filter-bar--all`은 조회 / 초기화 callback 조합 예시이고 입력/조회 적용 상태 구분은 숨김 `patterns-data-filter-bar--draft-applied`가 표시한다. DataTable의 `patterns-data-data-table--with-filter-bar-story`는 조회 버튼 적용 verification이다.
|
||||
- React 소비 CSS: [Tailwind v4 검색 범위와 HM source 유지](runtime-portability.md#21-tailwind-v4-소비자-검색-범위), [Runtime Theme 연결](runtime-theme.md)을 확인한다.
|
||||
- 실행 위치별 Asset: [Noto Sans KR·폰트 URL·단일 HTML 내장](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)을 확인한다.
|
||||
|
||||
|
||||
@@ -269,6 +269,10 @@ UI는 제거했다. 고정 표시 조건은 `createOrgChartExampleDataset`의
|
||||
소비 프로젝트는 이 옵션들을 그대로 사용할 수 있다. Story 브라우저 검증(P7·P8·P9·P9.2~P9.5,
|
||||
hierarchy, HM Story)은 이 고정 미리보기의 렌더링·접기·Zoom/Pan/Fit·HM Dialog만 확인하고,
|
||||
소속 변경·Drag & Drop ON/OFF 전환·진행 중 취소는 `verify-p9-3-core.mjs`·`verify-p9-4-core.mjs`의 Vanilla Core 검증이 담당한다.
|
||||
2026-10-23부터 Story는 Core 툴바의 안내 문구를 Story 전용 shadow stylesheet로 숨긴다. 기준일과
|
||||
선택 회사·인원 요약은 표시하지 않고, Drag & Drop 이동 안내는 시각적으로만 숨겨 live status·
|
||||
`aria-describedby`로 남긴다. DOM 요소와 접기·Zoom·Pan·Fit·HM Dialog 기능은 그대로다. Core·React 기본 표시와 공개 API는
|
||||
바뀌지 않으며 소비 프로젝트가 이 숨김 방식을 따라 할 필요는 없다.
|
||||
|
||||
Expanded Story의 외곽 미리보기는 실제 Center Storybook 공통 카드 CSS의
|
||||
`--sb-center-card-background` / `--sb-center-card-border` / `--sb-center-card-radius`를
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Data Table (Pattern)
|
||||
|
||||
> 2026-10-10 추가 수정 요청 기준: 기본 검색 가운데 / 추가 조건 검색 왼쪽, 조건→검색어→조회→초기화·8px. Header는 Card 밖, 검색·목록 도구·결과는 한 Card. 페이지 크기 label useId로 인스턴스 분리. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
||||
> 2026-10-10 추가 수정 요청 기준: 기본 검색 가운데 / 추가 조건 검색 왼쪽, 조건→검색어→조회→초기화·8px. Header는 Card 밖, 검색·목록 도구·결과는 한 Card(2026-10-22: 대표 Story의 목록 제목 `사용자 목록`은 CardTitle로 같은 Card 상단에 둔다). 페이지 크기 label useId로 인스턴스 분리. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다. 이 문서는 Data Table Pattern 고유의 API·상태·결정만 기록한다.
|
||||
|
||||
@@ -70,7 +70,7 @@ Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- `Patterns/Data/Data Table` → `Data Table`(대표: 선택·정렬·열 표시·행 액션·페이지 크기·페이지), `With Filter Bar`(숨김 Filter Bar 연동 검증). verification: `WithFilterBar`, `Loading`, `Empty`.
|
||||
- `Patterns/Data/Data Table` → `Data Table`(대표: Overview 선택·정렬·열 표시·행 액션·페이지 크기·페이지, `기본 목록 검색`, `추가 조건 목록 검색`), `With Filter Bar`(숨김 Filter Bar 연동 검증). 두 목록 검색 구역은 흰 Card 하나에 CardTitle(`사용자 목록`) → 검색·열 표시 도구 → 표 순서다. 페이지 제목 PageHeader를 쓰는 화면에서는 PageHeader를 이 Card 밖에 둔다. verification: `WithFilterBar`, `Loading`, `Empty`.
|
||||
- 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음).
|
||||
|
||||
## 10. Accessibility
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
Pattern은 상태가 없다. Data Table과 연결할 때는 검색 → `table.setGlobalFilter`, 열 선택 → `column.setFilterValue`로 소비자가 잇는다(`data-table.md` §6).
|
||||
|
||||
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)는 입력 조건 수와 조회 적용 조건 수를 구분하고 조회/초기화 callback을 실행한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 입력 즉시 table 필터를 변경하며 조회 버튼은 없다. 이 둘을 조회 버튼 적용 계약으로 해석하지 않는다.
|
||||
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)는 조회/초기화 callback을 실행하고, 입력 조건 수와 조회 적용 조건 수의 구분은 숨김 verification `DraftApplied`가 표시한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 조회 버튼(form submit)에서 table 필터를 적용하고 초기화로 비운다. 이 둘은 소비 조합 예시이며 FilterBar 자체의 조회 시점 계약으로 해석하지 않는다.
|
||||
|
||||
DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다. 부모가 각 `value`를 소유하고 초기화 시 `setDate(undefined)` / `setCategory(undefined)`로 비우므로, 선택값 초기화 목적의 `key` remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API는 유지하고 목록의 조회/초기화 버튼은 위 최신 소비 계약을 따른다. 외부 값 초기화는 자식 `onValueChange`를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.
|
||||
|
||||
@@ -124,9 +124,9 @@ Input, Select, Combobox, Date Picker, Button(그 외 Popover 등 자유). 각
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
`Patterns/Data/Filter Bar` → `Filter Bar`: 검색, 상태 Select, 분류 Combobox, 날짜 Date Picker, 초기화 / 조회, 적용된 조건 수.
|
||||
`Patterns/Data/Filter Bar` → `Filter Bar`: 검색, 상태 Select, 분류 Combobox, 날짜 Date Picker, 조회 / 초기화. 입력·적용 조건 수 표시는 숨김 verification `DraftApplied`(`patterns-data-filter-bar--draft-applied`)에만 있다. Usage / Do & Don't 규칙은 Story가 아니라 이 문서(§6.1, §9)에 둔다.
|
||||
|
||||
현재 대표 Story는 조건→검색어→조회→초기화, 조회 `variant="primary" appearance="solid"`·초기화 `variant="cancel" appearance="outline"`를 사용한다. 이는 [최신 목록 검색 계약](../guidelines/publisher-additional-review-2026-10-10.md)에 따른 Button 세트 예외다. FilterBar 자체가 Button appearance나 조회 시점을 강제하지 않는다. 대표 Story의 입력 조건 수는 즉시 변하지만 조회 적용 조건 수는 조회 클릭에만 반영되며 초기화는 둘 다 비운다. 업무 화면은 §6.1처럼 요청된 동작을 구현하며 Story의 부가 기능을 필수로 복사하지 않는다.
|
||||
현재 대표 Story는 조건→검색어→조회→초기화, 조회 `variant="primary" appearance="solid"`·초기화 `variant="cancel" appearance="outline"`를 사용한다. 이는 [최신 목록 검색 계약](../guidelines/publisher-additional-review-2026-10-10.md)에 따른 Button 세트 예외다. FilterBar 자체가 Button appearance나 조회 시점을 강제하지 않는다. 숨김 verification `DraftApplied`의 입력 조건 수는 즉시 변하지만 조회 적용 조건 수는 조회 클릭에만 반영되며 초기화는 둘 다 비운다(대표 Story는 상태값을 표시하지 않는다). 업무 화면은 §6.1처럼 요청된 동작을 구현하며 Story의 부가 기능을 필수로 복사하지 않는다.
|
||||
|
||||
## 10. Accessibility
|
||||
|
||||
|
||||
@@ -42,3 +42,15 @@
|
||||
- **Tabs → Do / Don't**: UI 예시 없이 문단만 있던 섹션을 제거했습니다. 규칙은 [Tabs §16](../components/tabs.md)에 있습니다.
|
||||
- **Button → States**: 대표 화면에서만 Brand / Standard 상태 설명 문단을 숨깁니다(Story 전용 `showNote`). 숨김 Story `States`·`StandardStates`는 문단을 유지하고, 규칙은 [Button](../components/button.md)과 UI AI 가이드에 있습니다. 모든 상태 버튼과 `Activated` 출력은 유지합니다.
|
||||
- 검증: tsc PASS, `test:guard` 94/94, `check:ai-docs` PASS, Story 359개 렌더·ID 동일, SW simplification 9/9. Expanded 설정 조작(직원 수·데이터 적용·LR 전환·Drag & Drop·초기화) 9/9. OrgChart P9(67/17)와 P9-2(2/4)는 da0e647 빌드와 같은 FAIL 목록이므로 기존 이슈입니다.
|
||||
|
||||
## 추가 간소화 2 (2026-10-22~23)
|
||||
|
||||
대표 Story의 페이지·섹션 설명과 예시 캡션을 더 줄였습니다. 위 `기준` 표와 `추가 간소화`의 "유지" 항목 중 아래는 이번 변경으로 바뀌었습니다(위 기록은 당시 결과로 보존).
|
||||
|
||||
- 컴포넌트 44개·Pattern 대표 Story: `StoryPageHeader`·`StorySection` 설명과 State note를 제거했습니다. 값 표시(`value = …`, `slide n / 5`, `Activated: n`, 선택 경로)·캡션·규격 목록은 Story 전용 `showValue`/`showNote`/`showSpec`/`showCaption`으로 대표 화면에서만 숨기고 숨김 verification Story는 유지합니다. 변경 내용은 각 `docs/components/*.md`·`docs/patterns/*.md`의 Storybook 절에 반영했습니다.
|
||||
- 숨김으로 옮긴 구역: Tabs `Default API` → `DefaultApi`, Filter Bar 입력/적용 조건 수 → `DraftApplied`(`scripts/verify-quality-reference.mjs`가 이 ID를 검사), Select 열린 목록 → 기존 숨김 `Open`. Toggle의 Controlled·className 확장 예시와 Filter Bar Usage / Do & Don't 문단은 제거했고 규칙은 각 문서에 있습니다.
|
||||
- Checkbox·Switch·Radio Group Hover는 Storybook 전용 `data-sb-hover`로 고정해 보여 줍니다(컴포넌트 API 아님).
|
||||
- Data Table 목록 검색 구역: 목록 제목을 Card 밖 PageHeader에서 같은 Card의 CardTitle로 옮겼습니다.
|
||||
- CENTER: 반복 경계 안내(`CENTER_BOUNDARY`)와 페이지 설명을 화면에서 제거했고 `Overview`는 숨김(직접 URL 유지)으로 바꿨습니다.
|
||||
- OrgChart: Overview·Full Composition·Expanded의 페이지·섹션 설명(가상 데이터 안내 포함)을 제거했습니다. Expanded는 Core 툴바 안내 문구를 Story 전용 stylesheet로 숨기며 기능은 그대로입니다([Expanded 사용법](../org-chart/expanded-usage.md)). `examples/org-chart/verify-p8.mjs`·`verify-p9.mjs`의 공통 스타일 비교에서 `.sb-page-description`은 기준 Pattern 화면과 OrgChart 화면 모두에 없습니다(양쪽 null). P8 `styles`의 Expanded 비교는 f4ddd5a 이후 Preview-only 구조(StorySection 없음)를 반영해 `.sb-page-section`·`-header`·`-title`이 **없어야** 하고 나머지 선택자는 Pattern 화면과 같아야 하도록 고쳤습니다. 2026-10-23 실행(6024 Storybook): P8 `styles` 8/8, `full-repair` 5/5(Expanded 수정 전 실행, 영향 없음), P9 `overview`·`department` 40/40 PASS, Console 오류 0.
|
||||
- Story ID는 바뀌지 않았고 `DefaultApi`·`DraftApplied` 두 숨김 Story가 추가되었습니다.
|
||||
@@ -219,11 +219,16 @@ try {
|
||||
}
|
||||
const reference = references[0].style;
|
||||
check(references.every(item => JSON.stringify(item.style) === JSON.stringify(reference)), 'Three existing Pattern pages share the measured StoryPage frame', references);
|
||||
// Expanded OrgChart is preview-only (no StorySection since f4ddd5a): its section elements must be absent (null),
|
||||
// every other selector still has to match the Pattern pages.
|
||||
const sectionless = ['.sb-page-section', '.sb-page-section-header', '.sb-page-section-title'];
|
||||
for (const story of stories) {
|
||||
await gotoStory(story);
|
||||
const style = await commonStyle();
|
||||
const previewOnly = story.file === 'expanded-org-chart';
|
||||
const expected = previewOnly ? { ...reference, ...Object.fromEntries(sectionless.map(selector => [selector, null])) } : reference;
|
||||
measurements.push({ name: story.name + ' common StoryPage computed styles', style });
|
||||
check(JSON.stringify(style) === JSON.stringify(reference), story.name + ': StoryPage title, spacing, section and inner styles match Pattern pages', { reference: patterns[0], difference: Object.keys(reference).filter(key => JSON.stringify(reference[key]) !== JSON.stringify(style[key])), style });
|
||||
check(JSON.stringify(style) === JSON.stringify(expected), story.name + (previewOnly ? ': StoryPage title and inner styles match Pattern pages; no section header' : ': StoryPage title, spacing, section and inner styles match Pattern pages'), { reference: patterns[0], difference: Object.keys(expected).filter(key => JSON.stringify(expected[key]) !== JSON.stringify(style[key])), style });
|
||||
await capture(story.file + '-1440');
|
||||
if (['full-composition', 'expanded-org-chart'].includes(story.file)) {
|
||||
const shielding = await page.evaluate(() => {
|
||||
|
||||
@@ -102,9 +102,11 @@ Standalone Tree/Summary factories keep their existing rendering paths.
|
||||
20/100/300 unique virtual employees and 3/5/7 companies. Run
|
||||
`node examples/org-chart/create-expanded-fixtures.mjs` to reproduce them.
|
||||
After build/prepare, open `http://127.0.0.1:4178/expanded.html`.
|
||||
Expanded OrgChart is one of the same four public Stories; it offers fixture and
|
||||
company controls without filtering JSON in the Story. Native Core controls are
|
||||
the explicit framework-independent HTML adapter, not imitations of HM React UI.
|
||||
Expanded OrgChart is one of the same four public Stories. Since 2026-10-22 it is
|
||||
a preview-only page (7 companies x 20 = 140 virtual employees, TB, Drag & Drop
|
||||
off, collapse / Zoom / Pan / Fit, HM Dialog) without setting panels. Fixture and
|
||||
company selection live in this `expanded.html` example; its native Core controls
|
||||
are the explicit framework-independent HTML adapter, not imitations of HM React UI.
|
||||
|
||||
|
||||
### Virtual example selection (P7)
|
||||
|
||||
@@ -36,7 +36,7 @@ try {
|
||||
}
|
||||
await task('FILTERBAR_REFERENCE_USAGE', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`${base}/storybook/iframe.html?id=patterns-data-filter-bar--all&viewMode=story`, { waitUntil: 'networkidle' });
|
||||
await page.goto(`${base}/storybook/iframe.html?id=patterns-data-filter-bar--draft-applied&viewMode=story`, { waitUntil: 'networkidle' });
|
||||
const status = page.getByRole('status');
|
||||
await page.getByRole('searchbox', { name: '검색어' }).fill('김');
|
||||
assert((await status.innerText()).includes('입력한 조건 1개 · 조회 적용 조건 0개'), 'draft / applied distinction');
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
TunnelReferenceDemo,
|
||||
} from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/facility.md and facility-assets.md (128 approved SVG references, shipped unchanged).
|
||||
// The display scale (Q2b) is HOLD and Facility has no Selected / Hover state.
|
||||
@@ -29,11 +29,8 @@ const page = (name: string, Demo: ComponentType): Story => ({
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title={name}
|
||||
description="승인 SVG Reference · 위치도 / 실사화"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
<StoryPageHeader title={name} />
|
||||
<StorySection title="Overview">
|
||||
{withRepresentation(() => <Demo />)}
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
@@ -52,16 +49,7 @@ export const Linked: Story = {
|
||||
...page("연계 시설물", LinkedFacilityReferenceDemo),
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-linked-page">
|
||||
<StoryPageHeader
|
||||
title="연계 시설물"
|
||||
description={
|
||||
<>
|
||||
승인 SVG Reference · 위치도 / 실사화
|
||||
<br />
|
||||
{CENTER_BOUNDARY}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StoryPageHeader title="연계 시설물" />
|
||||
{withRepresentation(() => <LinkedFacilityReferenceDemo />)}
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CenterInformationDisplayReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owner: ui-viz docs/patterns/center-information-presentation.md and center-information-{box,table,emphasis}.md.
|
||||
// Single-line ellipsis is resolved; multi-line overflow, exact placement and the presentation scale (Q3) are not.
|
||||
@@ -17,11 +17,8 @@ export const Information: Story = {
|
||||
name: "Information",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Information"
|
||||
description="Box · Table · Emphasis · 배율(Q3) 미정"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
<StoryPageHeader title="Information" />
|
||||
<StorySection title="Overview">
|
||||
{withRepresentation(() => (
|
||||
<div className="sb-center-information">
|
||||
<CenterInformationDisplayReferenceDemo />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, CenterFrame, SCALE_NOTE, StatusTable } from "./center-shared"
|
||||
import { 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.
|
||||
const STATUS: [string, string, string, string][] = [
|
||||
@@ -24,17 +24,19 @@ const meta = { title: "Center Graphic/Overview", parameters: { layout: "fullscre
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Hidden from the sidebar (verification); the Story ID and direct URL stay. Keeps the scale rule and the status registry.
|
||||
export const Overview: Story = {
|
||||
tags: ["verification"],
|
||||
name: "Overview",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Overview" description={CENTER_BOUNDARY} />
|
||||
<StoryPageHeader title="Overview" />
|
||||
<CenterFrame>
|
||||
<div className="sb-center-overview">
|
||||
<StorySection title="Representation Scale">
|
||||
<p className="sb-center-note">{SCALE_NOTE}</p>
|
||||
</StorySection>
|
||||
<StorySection title="Status" description="항목별 확정 상태와 Storybook 표현">
|
||||
<StorySection title="Status">
|
||||
<div className="sb-center-scroll">
|
||||
<StatusTable rows={STATUS} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,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 { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/route.md, spatial-map.md, spatial-section.md, composition.md.
|
||||
// Route layer order Shadow -> Outline -> Primary Stroke is CONFIRMED; the whole-map layer order is Candidate and line join is TBD.
|
||||
@@ -19,11 +19,8 @@ export const Spatial: Story = {
|
||||
name: "Spatial",
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-spatial-page">
|
||||
<StoryPageHeader
|
||||
title="Spatial"
|
||||
description="노선 레이어 순서 확정 · 전체 지도 레이어 순서 Candidate · line join TBD"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
<StoryPageHeader title="Spatial" />
|
||||
<StorySection title="Overview">
|
||||
{withRepresentation(() => <CenterSpatialInformationReferenceDemo />)}
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,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 { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
import { 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.
|
||||
const meta = {
|
||||
@@ -20,11 +20,8 @@ export const Compositions: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Title"
|
||||
description="4가지 Type · Resolution B 수치 TBD · palette SCOPED_DESIGN_APPROVAL"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
<StoryPageHeader title="Title" />
|
||||
<StorySection title="Overview">
|
||||
{withRepresentation(() => <CenterTitleReferenceDemo />)}
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -103,11 +103,11 @@ function LongSpecimen() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "single", label: "Single", note: "type=\"single\": 한 번에 하나, 기본으로 첫 항목이 열림", node: <SingleSpecimen /> },
|
||||
{ id: "collapsible-single", label: "Collapsible Single", note: "type=\"single\" collapsible", node: <CollapsibleSingleSpecimen /> },
|
||||
{ id: "multiple", label: "Multiple", note: "type=\"multiple\": 동시에 여러 항목", node: <MultipleSpecimen /> },
|
||||
{ id: "disabled", label: "Disabled Item", note: "disabled 항목은 열리지 않고 키보드 이동에서 제외", node: <DisabledSpecimen /> },
|
||||
{ id: "long", label: "Long Content", note: "여러 줄 Trigger와 긴 콘텐츠", node: <LongSpecimen /> },
|
||||
{ id: "single", label: "Single", node: <SingleSpecimen /> },
|
||||
{ id: "collapsible-single", label: "Collapsible Single", node: <CollapsibleSingleSpecimen /> },
|
||||
{ id: "multiple", label: "Multiple", node: <MultipleSpecimen /> },
|
||||
{ id: "disabled", label: "Disabled Item", node: <DisabledSpecimen /> },
|
||||
{ id: "long", label: "Long Content", node: <LongSpecimen /> },
|
||||
]
|
||||
|
||||
// No `component` here: Accordion props are a union (single | multiple), so Stories without args would not type-check.
|
||||
@@ -116,7 +116,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={360} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: s.node }))} />
|
||||
<StateBoard min={360} states={sections.filter((s) => 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.
|
||||
@@ -125,10 +125,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Accordion"
|
||||
description="동작은 Radix API · HM variant prop 없음"
|
||||
/>
|
||||
<StoryPageHeader title="Accordion" />
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Variants">
|
||||
{board("collapsible-single", "multiple")}
|
||||
|
||||
@@ -73,14 +73,11 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Alert Dialog"
|
||||
description="닫기(X)·바깥 클릭 닫기 없음"
|
||||
/>
|
||||
<StorySection title="Overview" description="같은 크기 Solid 버튼 세트">
|
||||
<StoryPageHeader title="Alert Dialog" />
|
||||
<StorySection title="Overview">
|
||||
<Confirmation />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="취소·삭제 같은 크기 Standard Solid">
|
||||
<StorySection title="Variants">
|
||||
<Destructive />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -26,10 +26,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Alert"
|
||||
description="인라인 메시지(Toast 아님) · default / destructive만 · 닫기 없음"
|
||||
/>
|
||||
<StoryPageHeader title="Alert" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={320}
|
||||
|
||||
@@ -49,8 +49,8 @@ const sections = [
|
||||
{ id: "r169", label: "16:9", node: <Image ratio={16 / 9} label="16:9" /> },
|
||||
{ id: "r43", label: "4:3", node: <Image ratio={4 / 3} label="4:3" /> },
|
||||
{ id: "r11", label: "1:1", node: <Image ratio={1} label="1:1" /> },
|
||||
{ id: "video", label: "Video-like placeholder", node: <Placeholder /> },
|
||||
{ id: "card", label: "Composed in a Card (consumer composition)", node: <InCard /> },
|
||||
{ id: "video", label: "Video", node: <Placeholder /> },
|
||||
{ id: "card", label: "Card", node: <InCard /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Data & Display/Aspect Ratio", component: AspectRatio, parameters: { layout: "padded" } } satisfies Meta<typeof AspectRatio>
|
||||
@@ -67,12 +67,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Aspect Ratio"
|
||||
description="폭에 따라 비율대로 높이 결정"
|
||||
/>
|
||||
<StorySection title="Overview" description="ratio = 16 / 9.">{board("r169")}</StorySection>
|
||||
<StorySection title="Variants" description="ratio 4 / 3 · 1 · 영상 자리">{board("r43", "r11", "video")}</StorySection>
|
||||
<StoryPageHeader title="Aspect Ratio" />
|
||||
<StorySection title="Overview">{board("r169")}</StorySection>
|
||||
<StorySection title="Variants">{board("r43", "r11", "video")}</StorySection>
|
||||
<StorySection title="Usage">{board("card")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -28,7 +28,8 @@ function Sizes() {
|
||||
)
|
||||
}
|
||||
|
||||
function Fallbacks() {
|
||||
// `showCaption={false}` drops the captions on the representative page (they restate the visible fallback text); the verification Story keeps them.
|
||||
function Fallbacks({ showCaption = true }: { showCaption?: boolean }) {
|
||||
const items = [
|
||||
{ label: "한글 두 글자", text: "김한" },
|
||||
{ label: "영문 두 글자", text: "HM" },
|
||||
@@ -43,7 +44,7 @@ function Fallbacks() {
|
||||
<AvatarImage src="/broken-avatar-image.png" alt="" />
|
||||
<AvatarFallback>{i.text}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-xs text-muted-foreground">{i.label}</span>
|
||||
{showCaption ? <span className="text-xs text-muted-foreground">{i.label}</span> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -66,8 +67,8 @@ function Group() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "image", label: "Image", note: "이미지가 로드되면 fallback은 나타나지 않습니다", node: <Sizes /> },
|
||||
{ id: "fallback", label: "Fallback (image error)", note: "이미지 로드 실패 시 initials", node: <Fallbacks /> },
|
||||
{ id: "image", label: "Image", node: <Sizes /> },
|
||||
{ id: "fallback", label: "Fallback", node: <Fallbacks showCaption={false} /> },
|
||||
{ id: "composition", label: "Composition", node: <Group /> },
|
||||
]
|
||||
|
||||
@@ -76,7 +77,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={320} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: s.node }))} />
|
||||
<StateBoard min={320} states={sections.filter((s) => 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.
|
||||
@@ -85,10 +86,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Avatar"
|
||||
description="이미지 실패 시 fallback 글자"
|
||||
/>
|
||||
<StoryPageHeader title="Avatar" />
|
||||
<StorySection title="Overview">{board("image")}</StorySection>
|
||||
<StorySection title="States">{board("fallback")}</StorySection>
|
||||
<StorySection title="Usage">{board("composition")}</StorySection>
|
||||
|
||||
@@ -28,14 +28,11 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Badge"
|
||||
description="Button variant와 별도인 컴포넌트"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본값: primary">
|
||||
<StoryPageHeader title="Badge" />
|
||||
<StorySection title="Overview">
|
||||
<Badge>Badge</Badge>
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="default(primary) / secondary(neutral) / destructive / outline.">
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
min={280}
|
||||
states={[
|
||||
@@ -76,7 +73,7 @@ export const All: Story = {
|
||||
{ id: "long", label: "Long text", children: <div className="max-w-[160px]"><Badge>A very long badge label</Badge></div> },
|
||||
{
|
||||
id: "link",
|
||||
label: "asChild (link)",
|
||||
label: "Link",
|
||||
children: (
|
||||
<Badge asChild variant="outline">
|
||||
<a href="#badge">Link badge</a>
|
||||
|
||||
@@ -112,10 +112,10 @@ function CustomLink() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "basic", label: "Basic (3 depth)", node: <Basic /> },
|
||||
{ id: "ellipsis", label: "Ellipsis", note: "BreadcrumbEllipsis는 표시용입니다. 메뉴 연결은 소비 조합(Dropdown Menu)", node: <Ellipsis /> },
|
||||
{ id: "long", label: "Long path", note: "좁은 폭에서 줄바꿈", node: <Long /> },
|
||||
{ id: "custom", label: "Custom link (asChild) and separator", node: <CustomLink /> },
|
||||
{ id: "basic", label: "Basic", node: <Basic /> },
|
||||
{ id: "ellipsis", label: "Ellipsis", node: <Ellipsis /> },
|
||||
{ id: "long", label: "Long path", node: <Long /> },
|
||||
{ id: "custom", label: "Custom link · separator", node: <CustomLink /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Navigation/Breadcrumb", component: Breadcrumb, parameters: { layout: "padded" } } satisfies Meta<typeof Breadcrumb>
|
||||
@@ -123,7 +123,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={420} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: narrow(s.node) }))} />
|
||||
<StateBoard min={420} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: narrow(s.node) }))} />
|
||||
)
|
||||
|
||||
// Representative Story: the common Story page; the sections group the same specimens.
|
||||
@@ -132,12 +132,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Breadcrumb"
|
||||
description="마지막 항목 = 현재 페이지"
|
||||
/>
|
||||
<StoryPageHeader title="Breadcrumb" />
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants" description="Ellipsis · 사용자 링크 · 구분자">{board("ellipsis", "custom")}</StorySection>
|
||||
<StorySection title="Variants">{board("ellipsis", "custom")}</StorySection>
|
||||
<StorySection title="Usage">{board("long")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -71,24 +71,32 @@ const stack = "grid gap-6"
|
||||
const heading = "text-base font-bold"
|
||||
const note = "text-sm text-muted-foreground"
|
||||
|
||||
const appearances = [
|
||||
["brand", "Brand"],
|
||||
["solid", "Standard Solid"],
|
||||
["outline", "Standard Outline"],
|
||||
] as const
|
||||
|
||||
function RoleRow({ appearance }: { appearance: (typeof appearances)[number][0] }) {
|
||||
return (
|
||||
<div className={row}>
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<Button key={variant} variant={variant} appearance={appearance}>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RolesBoard() {
|
||||
return (
|
||||
<div className={stack}>
|
||||
{(
|
||||
[
|
||||
["brand", "Brand"],
|
||||
["solid", "Standard Solid"],
|
||||
["outline", "Standard Outline"],
|
||||
] as const
|
||||
).map(([appearance, title]) => (
|
||||
{appearances.map(([appearance, title]) => (
|
||||
<section key={appearance}>
|
||||
<h3 className={heading}>{title}</h3>
|
||||
<div className={`${row} mt-3`}>
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<Button key={variant} variant={variant} appearance={appearance}>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
<div className="mt-3">
|
||||
<RoleRow appearance={appearance} />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
@@ -118,7 +126,7 @@ function SizesBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
// `showNote={false}` keeps the representative page UI-only; the verification Stories keep the state notes.
|
||||
// `showNote={false}` keeps the representative page UI-only; the verification Story keeps the note and the Activated readout.
|
||||
function BrandStates({ showNote = true }: { showNote?: boolean }) {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
@@ -132,7 +140,7 @@ function BrandStates({ showNote = true }: { showNote?: boolean }) {
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<output aria-live="polite">Activated: {count}</output>
|
||||
{showNote ? <output aria-live="polite">Activated: {count}</output> : null}
|
||||
{showNote ? (
|
||||
<p className={note}>
|
||||
Hover와 Pressed는 Storybook이 고정한 상태입니다. Interact 위에서 실제 Hover·Pressed·포인터 이탈을 확인합니다. Disabled는 실행되지 않습니다.
|
||||
@@ -149,15 +157,17 @@ const standardStates = [
|
||||
["Disabled", { disabled: true }],
|
||||
] as const
|
||||
|
||||
function StandardStateTable({ appearance, title }: { appearance: "solid" | "outline"; title: string }) {
|
||||
// Without `title` the table sits under a StateBoard label (representative page) and drops the Role column:
|
||||
// every button already shows its role name, so the row header would repeat it.
|
||||
function StandardStateTable({ appearance, title }: { appearance: "solid" | "outline"; title?: string }) {
|
||||
return (
|
||||
<section className="min-w-0">
|
||||
<h3 className={heading}>{title}</h3>
|
||||
<div className="mt-3 min-w-0 max-w-full overflow-x-auto">
|
||||
{title ? <h3 className={heading}>{title}</h3> : null}
|
||||
<div className={`${title ? "mt-3 " : ""}min-w-0 max-w-full overflow-x-auto`}>
|
||||
<table className="w-full border-collapse [&_td]:p-3 [&_th]:p-3 [&_th]:text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Role</th>
|
||||
{title ? <th scope="col">Role</th> : null}
|
||||
{standardStates.map(([state]) => (
|
||||
<th scope="col" key={state}>
|
||||
{state}
|
||||
@@ -168,7 +178,7 @@ function StandardStateTable({ appearance, title }: { appearance: "solid" | "outl
|
||||
<tbody>
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<tr key={variant}>
|
||||
<th scope="row">{label}</th>
|
||||
{title ? <th scope="row">{label}</th> : null}
|
||||
{standardStates.map(([state, props]) => (
|
||||
<td key={state}>
|
||||
<Button variant={variant} appearance={appearance} {...props}>
|
||||
@@ -227,22 +237,22 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Button" description="variant = 역할(색) · appearance = Brand / Standard Solid / Standard Outline · size = 높이" />
|
||||
<StorySection title="Overview" description="기본값: Brand · Primary · Medium 34px">
|
||||
<StoryPageHeader title="Button" />
|
||||
<StorySection title="Overview">
|
||||
<Button>Button</Button>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="28 / 34 / 42px · 위 Brand, 아래 Standard Solid">
|
||||
<StorySection title="Sizes">
|
||||
<SizesBoard />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="appearance별 허용 역할 조합">
|
||||
<RolesBoard />
|
||||
<StorySection title="Variants">
|
||||
<StateBoard states={appearances.map(([appearance, label]) => ({ id: `roles-${appearance}`, label, wide: true, children: <RoleRow appearance={appearance} /> }))} />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Hover·Pressed는 Storybook 고정 상태">
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
min={1200}
|
||||
states={[
|
||||
{ id: "brand-states", label: "Brand", children: <BrandStates showNote={false} /> },
|
||||
{ id: "standard-states", label: "Standard Solid · Standard Outline", children: <StandardStatesBoard showNote={false} /> },
|
||||
{ id: "brand-states", label: "Brand", wide: true, children: <BrandStates showNote={false} /> },
|
||||
{ id: "standard-solid-states", label: "Standard Solid", wide: true, children: <StandardStateTable appearance="solid" /> },
|
||||
{ id: "standard-outline-states", label: "Standard Outline", wide: true, children: <StandardStateTable appearance="outline" /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
|
||||
@@ -14,12 +14,13 @@ const month = new Date(2026, 9, 1)
|
||||
|
||||
const box = (node: ReactNode) => <div className="rounded-md border bg-card w-fit">{node}</div>
|
||||
|
||||
function Single() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Single({ showValue = true }: { showValue?: boolean }) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date(2026, 9, 15))
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{box(<Calendar mode="single" selected={date} onSelect={setDate} defaultMonth={month} today={today} />)}
|
||||
<span className="text-sm text-muted-foreground" data-testid="single">{`선택: ${date ? date.toLocaleDateString("ko-KR") : "없음"}`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="single">{`선택: ${date ? date.toLocaleDateString("ko-KR") : "없음"}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -48,8 +49,8 @@ function TwoMonths() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "single", label: "Single (today = 10/2, selected = 10/15)", node: <Single /> },
|
||||
{ id: "disabled", label: "Disabled dates (before 10/5, Sundays)", node: <DisabledDates /> },
|
||||
{ id: "single", label: "Single", node: <Single showValue={false} /> },
|
||||
{ id: "disabled", label: "Disabled dates", node: <DisabledDates /> },
|
||||
{ id: "range", label: "Range", node: <Range /> },
|
||||
{ id: "months", label: "Two months", node: <TwoMonths /> },
|
||||
]
|
||||
@@ -69,12 +70,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Calendar"
|
||||
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>
|
||||
<StoryPageHeader title="Calendar" />
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Variants">{board("range", "months")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -70,19 +70,16 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Card"
|
||||
description="Default 상태만 정의"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본값: White">
|
||||
<StateBoard min={360} states={[{ id: "surface", label: "Surface · White (기본)", children: <Specimen /> }]} />
|
||||
<StoryPageHeader title="Card" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard min={360} states={[{ id: "surface", label: "White", children: <Specimen /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="content(Beige) · 제목 옆 Action">
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
min={360}
|
||||
states={[
|
||||
{ id: "content", label: "Surface · Content (Beige)", children: <Specimen surface="content" /> },
|
||||
{ id: "action", label: "Title + Action", noteAfter: true, note: "Action은 Title 반대쪽 끝, 교차축 중앙 정렬. Title과 20px 이상 간격.", children: <Specimen action /> },
|
||||
{ id: "content", label: "Content (Beige)", children: <Specimen surface="content" /> },
|
||||
{ id: "action", label: "Title + Action", children: <Specimen action /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
|
||||
@@ -37,7 +37,8 @@ function useReadout() {
|
||||
return { setApi, current, count }
|
||||
}
|
||||
|
||||
function Basic() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Basic({ showValue = true }: { showValue?: boolean }) {
|
||||
const { setApi, current, count } = useReadout()
|
||||
return frame(
|
||||
<div className="grid gap-2">
|
||||
@@ -52,7 +53,7 @@ function Basic() {
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
<span className="text-center text-sm text-muted-foreground" data-testid="readout">{`slide ${current} / ${count}`}</span>
|
||||
{showValue ? <span className="text-center text-sm text-muted-foreground" data-testid="readout">{`slide ${current} / ${count}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -73,7 +74,7 @@ function Multiple() {
|
||||
)
|
||||
}
|
||||
|
||||
function Loop() {
|
||||
function Loop({ showValue = true }: { showValue?: boolean }) {
|
||||
const { setApi, current, count } = useReadout()
|
||||
return frame(
|
||||
<div className="grid gap-2">
|
||||
@@ -88,7 +89,7 @@ function Loop() {
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
<span className="text-center text-sm text-muted-foreground" data-testid="loop-readout">{`slide ${current} / ${count}`}</span>
|
||||
{showValue ? <span className="text-center text-sm text-muted-foreground" data-testid="loop-readout">{`slide ${current} / ${count}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -130,11 +131,11 @@ function Media() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "basic", label: "Basic (single item) + setApi readout", node: <Basic /> },
|
||||
{ id: "multiple", label: "Multiple items (basis-1/2)", node: <Multiple /> },
|
||||
{ id: "loop", label: "Loop (controls never disable)", node: <Loop /> },
|
||||
{ id: "basic", label: "Basic", node: <Basic showValue={false} /> },
|
||||
{ id: "multiple", label: "Multiple items", node: <Multiple /> },
|
||||
{ id: "loop", label: "Loop", node: <Loop showValue={false} /> },
|
||||
{ id: "vertical", label: "Vertical", node: <Vertical /> },
|
||||
{ id: "media", label: "Media (AspectRatio composition)", node: <Media /> },
|
||||
{ id: "media", label: "Media", node: <Media /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Data & Display/Carousel", component: Carousel, parameters: { layout: "padded" } } satisfies Meta<typeof Carousel>
|
||||
@@ -151,12 +152,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Carousel"
|
||||
description="슬라이드 폭(basis-*)은 CarouselItem className"
|
||||
/>
|
||||
<StoryPageHeader title="Carousel" />
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants" description="여러 항목 · loop · 세로">{board("multiple", "loop", "vertical")}</StorySection>
|
||||
<StorySection title="Variants">{board("multiple", "loop", "vertical")}</StorySection>
|
||||
<StorySection title="Usage">{board("media")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -17,10 +17,11 @@ function Option({ label, children }: { label: string; children: (id: string) =>
|
||||
)
|
||||
}
|
||||
|
||||
// Starts indeterminate; a click makes it checked (Radix), so the Indeterminate example is also the interactive one.
|
||||
function Controlled() {
|
||||
const [checked, setChecked] = useState<boolean | "indeterminate">("indeterminate")
|
||||
return (
|
||||
<Option label={`Select all (${String(checked)})`}>
|
||||
<Option label="Option">
|
||||
{(id) => <Checkbox id={id} checked={checked} onCheckedChange={setChecked} />}
|
||||
</Option>
|
||||
)
|
||||
@@ -47,18 +48,14 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Checkbox"
|
||||
description="18px 박스 · Checked·Indeterminate는 state-active 색"
|
||||
/>
|
||||
<StoryPageHeader title="Checkbox" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={240}
|
||||
states={[
|
||||
{ id: "unchecked", label: "Unchecked", children: <Option label="Option">{(id) => <Checkbox id={id} />}</Option> },
|
||||
{ id: "checked", label: "Checked", children: <Option label="Option">{(id) => <Checkbox id={id} defaultChecked />}</Option> },
|
||||
{ id: "indeterminate", label: "Indeterminate", children: <Option label="Option">{(id) => <Checkbox id={id} checked="indeterminate" />}</Option> },
|
||||
{ id: "interactive", label: "Controlled · click toggles", noteAfter: true, note: "Indeterminate에서 클릭하면 checked가 됩니다(Radix).", children: <Controlled /> },
|
||||
{ id: "indeterminate", label: "Indeterminate", children: <Controlled /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -66,7 +63,17 @@ export const All: Story = {
|
||||
<StateBoard
|
||||
min={240}
|
||||
states={[
|
||||
{ id: "hover", label: "Hover", noteAfter: true, note: "Unchecked는 테두리가 primary 색, Checked는 Black 20% overlay. 포인터를 올려 확인합니다.", children: <Option label="Option">{(id) => <Checkbox id={id} />}</Option> },
|
||||
{
|
||||
id: "hover",
|
||||
label: "Hover",
|
||||
// data-sb-hover pins :hover (Storybook only, .storybook/pseudo-states.ts).
|
||||
children: (
|
||||
<div className="grid gap-3">
|
||||
<Option label="Unchecked">{(id) => <Checkbox id={id} data-sb-hover="" />}</Option>
|
||||
<Option label="Checked">{(id) => <Checkbox id={id} defaultChecked data-sb-hover="" />}</Option>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ id: "focus", label: "Focus", children: <Option label="Option">{(id) => <Checkbox id={id} />}</Option> },
|
||||
{
|
||||
id: "disabled",
|
||||
@@ -81,7 +88,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리">
|
||||
<StorySection title="Validation">
|
||||
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Option label="Option">{(id) => <Checkbox id={id} aria-invalid />}</Option> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Usage">
|
||||
|
||||
@@ -63,7 +63,7 @@ const sections = [
|
||||
{ id: "open", label: "Open", node: <OpenSpecimen /> },
|
||||
{ id: "disabled", label: "Disabled", node: <DisabledSpecimen /> },
|
||||
{ id: "long", label: "Long Content", node: <LongSpecimen /> },
|
||||
{ id: "controlled", label: "Controlled + Button trigger", note: "asChild: Button이 trigger이며 HM Button 스타일 유지", node: <ControlledSpecimen /> },
|
||||
{ id: "controlled", label: "Button trigger", node: <ControlledSpecimen /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Disclosure/Collapsible", component: Collapsible, parameters: { layout: "padded" } } satisfies Meta<typeof Collapsible>
|
||||
@@ -71,7 +71,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={360} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: s.node }))} />
|
||||
<StateBoard min={360} states={sections.filter((s) => 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.
|
||||
@@ -80,10 +80,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Collapsible"
|
||||
description="구분선·애니메이션 없음"
|
||||
/>
|
||||
<StoryPageHeader title="Collapsible" />
|
||||
<StorySection title="Overview">{board("closed")}</StorySection>
|
||||
<StorySection title="States">{board("open", "disabled")}</StorySection>
|
||||
<StorySection title="Usage">{board("long", "controlled")}</StorySection>
|
||||
|
||||
@@ -40,14 +40,15 @@ function Empty() {
|
||||
)
|
||||
}
|
||||
|
||||
function Controlled() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Controlled({ showValue = true }: { showValue?: boolean }) {
|
||||
const [value, setValue] = useState("nuxt")
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Field label="선택됨 (controlled)" id="cb-controlled">
|
||||
<Combobox id="cb-controlled" controlled options={frameworks} value={value} onValueChange={setValue} />
|
||||
</Field>
|
||||
<span className="text-sm text-muted-foreground" data-testid="controlled">{`value = ${value}`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="controlled">{`value = ${value}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -99,9 +100,8 @@ function SelectComparison() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "empty", label: "Empty placeholder + search (Remix is a disabled option)", node: <Empty /> },
|
||||
{ id: "controlled", label: "Selected, controlled", node: <Controlled /> },
|
||||
{ id: "medium", label: "Medium size", node: <Medium /> },
|
||||
{ id: "empty", label: "Placeholder", node: <Empty /> },
|
||||
{ id: "controlled", label: "Selected", node: <Controlled showValue={false} /> },
|
||||
{ id: "long", label: "Long option", node: <LongLabel /> },
|
||||
{ id: "disabled", label: "Disabled", node: <Disabled /> },
|
||||
]
|
||||
@@ -121,19 +121,16 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Combobox"
|
||||
description="Popover + Command · 짧은 목록은 Select 사용"
|
||||
/>
|
||||
<StorySection title="Overview" description="Escape로 닫으면 Trigger로 focus 복귀">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="기본값: Large · 같은 size의 Select와 비교">
|
||||
<StateBoard
|
||||
min={320}
|
||||
states={[
|
||||
...sections.filter((s) => s.id === "medium").map((s) => ({ id: s.id, label: s.label, children: s.node })),
|
||||
{ id: "select-comparison", label: "Select / Combobox visual comparison", wide: true, children: <SelectComparison /> },
|
||||
]}
|
||||
/>
|
||||
<StoryPageHeader title="Combobox" />
|
||||
<StorySection title="Overview">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes">
|
||||
{/* The field label already names the size, so the Medium specimen has no separate example title. */}
|
||||
<div className="grid gap-10">
|
||||
<div className="max-w-sm">
|
||||
<Medium />
|
||||
</div>
|
||||
<StateBoard min={320} states={[{ id: "select-comparison", label: "Select / Combobox", wide: true, children: <SelectComparison /> }]} />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="States">{board("controlled", "disabled")}</StorySection>
|
||||
<StorySection title="Usage">{board("long")}</StorySection>
|
||||
|
||||
@@ -11,7 +11,8 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
|
||||
|
||||
const box = (node: ReactNode) => <div className="w-full max-w-sm rounded-md border">{node}</div>
|
||||
|
||||
function Basic() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Basic({ showValue = true }: { showValue?: boolean }) {
|
||||
const [last, setLast] = useState("-")
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
@@ -29,7 +30,7 @@ function Basic() {
|
||||
</CommandList>
|
||||
</Command>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground" data-testid="last">{`선택: ${last}`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="last">{`선택: ${last}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -80,9 +81,9 @@ function Long() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "basic", label: "Search + disabled item", node: <Basic /> },
|
||||
{ id: "grouped", label: "Groups, icons, shortcut", node: <Grouped /> },
|
||||
{ id: "long", label: "Long list (list scrolls)", node: <Long /> },
|
||||
{ id: "basic", label: "Search", node: <Basic showValue={false} /> },
|
||||
{ id: "grouped", label: "Groups", node: <Grouped /> },
|
||||
{ id: "long", label: "Long list", node: <Long /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Search & Date/Command", component: Command, parameters: { layout: "padded" } } satisfies Meta<typeof Command>
|
||||
@@ -99,10 +100,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Command"
|
||||
description="Combobox와 명령 팔레트의 기반"
|
||||
/>
|
||||
<StoryPageHeader title="Command" />
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants">{board("grouped")}</StorySection>
|
||||
<StorySection title="Usage">{board("long")}</StorySection>
|
||||
|
||||
@@ -82,11 +82,8 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Context Menu"
|
||||
description="오른쪽 클릭 · Shift+F10 · 길게 누르기로 열기"
|
||||
/>
|
||||
<StorySection title="Overview" description="점선 영역에서 오른쪽 클릭">
|
||||
<StoryPageHeader title="Context Menu" />
|
||||
<StorySection title="Overview">
|
||||
<Full />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -52,14 +52,15 @@ function FormatExamples() {
|
||||
)
|
||||
}
|
||||
|
||||
function Controlled() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Controlled({ showValue = true }: { showValue?: boolean }) {
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Field label="마감일 (controlled)" id="dp-controlled">
|
||||
<DatePicker id="dp-controlled" controlled value={date} onValueChange={setDate} calendarProps={calendarProps} />
|
||||
</Field>
|
||||
<span className="text-sm text-muted-foreground" data-testid="controlled">{`value = ${date ? date.toLocaleDateString("ko-KR") : "undefined"}`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="controlled">{`value = ${date ? date.toLocaleDateString("ko-KR") : "undefined"}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -92,11 +93,10 @@ function Disabled() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "empty", label: "Empty (placeholder)", node: <Empty /> },
|
||||
{ id: "empty", label: "Empty", node: <Empty /> },
|
||||
{ id: "selected", label: "Selected", node: <Selected /> },
|
||||
{ id: "format", label: "Default format / custom override", node: <FormatExamples /> },
|
||||
{ id: "controlled", label: "Controlled", node: <Controlled /> },
|
||||
{ id: "medium", label: "Medium size", node: <Medium /> },
|
||||
{ id: "format", label: "Format", node: <FormatExamples /> },
|
||||
{ id: "controlled", label: "Controlled", node: <Controlled showValue={false} /> },
|
||||
{ id: "disabled-dates", label: "Disabled dates", node: <DisabledDates /> },
|
||||
{ id: "disabled", label: "Disabled", node: <Disabled /> },
|
||||
]
|
||||
@@ -115,12 +115,14 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Date Picker"
|
||||
description="Popover + Calendar · 날짜 선택 시 닫힘"
|
||||
/>
|
||||
<StoryPageHeader title="Date Picker" />
|
||||
<StorySection title="Overview">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="기본값: Large">{board("medium")}</StorySection>
|
||||
{/* The field label already names the size, so the Medium specimen has no separate example title. */}
|
||||
<StorySection title="Sizes">
|
||||
<div className="max-w-sm">
|
||||
<Medium />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="States">{board("selected", "controlled", "disabled-dates", "disabled")}</StorySection>
|
||||
<StorySection title="Usage">{board("format")}</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -184,17 +184,17 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
title="Dialog"
|
||||
/>
|
||||
<StorySection title="Overview" description="내용 기준 크기">
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="긴 내용은 Body만 scroll · Header 70 / Footer 48px 제외">
|
||||
<StorySection title="Variants">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<LongContent />
|
||||
<WideContent />
|
||||
<BodyScroll />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="취소·실행은 Standard Solid 세트">
|
||||
<StorySection title="Usage">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<DeleteConfirmation />
|
||||
<FormContent />
|
||||
|
||||
@@ -100,14 +100,11 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Drawer"
|
||||
description="핸들·패널을 아래로 끌어 닫기"
|
||||
/>
|
||||
<StorySection title="Overview" description="데스크톱: 우측 가로 취소 → 실행 · 640px 이하: 세로 실행 위 / 취소 아래">
|
||||
<StoryPageHeader title="Drawer" />
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
|
||||
<StorySection title="Usage">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<FormCase />
|
||||
<LongCase />
|
||||
|
||||
@@ -87,7 +87,7 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
title="Dropdown Menu"
|
||||
/>
|
||||
<StorySection title="Overview" description="항목 · Disabled · 구분선 · Checkbox · Radio · 하위 메뉴 · 위험 항목">
|
||||
<StorySection title="Overview">
|
||||
<Full />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -53,14 +53,11 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Hover Card"
|
||||
description="링크 hover / focus 시 미리보기"
|
||||
/>
|
||||
<StoryPageHeader title="Hover Card" />
|
||||
<StorySection title="Overview">
|
||||
<Preview />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="카드 폭 고정 · 높이만 늘어남">
|
||||
<StorySection title="Usage">
|
||||
<LongContent />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -59,35 +59,41 @@ const sizes: { size: InputSize; name: string; height: number; padding: number; t
|
||||
{ size: "large", name: "Large", height: 42, padding: 12, typography: "16 / 24 / 400", usage: "Component default" },
|
||||
]
|
||||
|
||||
function SizeCard({ spec }: { spec: (typeof sizes)[number] }) {
|
||||
function SizeCard({ spec, showSpec = true }: { spec: (typeof sizes)[number]; showSpec?: boolean }) {
|
||||
return (
|
||||
<section className="grid content-start gap-4" data-input-size={spec.size}>
|
||||
<Specimen size={spec.size} label={spec.name} defaultValue={`${spec.name} ${spec.height}px`} />
|
||||
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-muted-foreground">Height</dt>
|
||||
<dd className="m-0">{spec.height}px</dd>
|
||||
<dt className="text-muted-foreground">Padding</dt>
|
||||
<dd className="m-0">0 / {spec.padding}px</dd>
|
||||
<dt className="text-muted-foreground">Typography</dt>
|
||||
<dd className="m-0">{spec.typography}</dd>
|
||||
<dt className="text-muted-foreground">Usage</dt>
|
||||
<dd className="m-0">{spec.usage}</dd>
|
||||
</dl>
|
||||
{/* Without the spec list the value would repeat the size name, so it shows sample text instead. */}
|
||||
<Specimen size={spec.size} label={spec.name} defaultValue={showSpec ? `${spec.name} ${spec.height}px` : "Hanmac Project"} />
|
||||
{showSpec ? (
|
||||
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-muted-foreground">Height</dt>
|
||||
<dd className="m-0">{spec.height}px</dd>
|
||||
<dt className="text-muted-foreground">Padding</dt>
|
||||
<dd className="m-0">0 / {spec.padding}px</dd>
|
||||
<dt className="text-muted-foreground">Typography</dt>
|
||||
<dd className="m-0">{spec.typography}</dd>
|
||||
<dt className="text-muted-foreground">Usage</dt>
|
||||
<dd className="m-0">{spec.usage}</dd>
|
||||
</dl>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function SizeComparison() {
|
||||
// `showSpec={false}` keeps the representative page UI-only; the verification Story keeps the spec lists and the size note.
|
||||
function SizeComparison({ showSpec = true }: { showSpec?: boolean }) {
|
||||
return (
|
||||
<div className="grid max-w-[1120px] gap-6">
|
||||
<div className="grid gap-6 [grid-template-columns:repeat(auto-fit,minmax(240px,1fr))]">
|
||||
{sizes.map((spec) => (
|
||||
<SizeCard key={spec.size} spec={spec} />
|
||||
<SizeCard key={spec.size} spec={spec} showSpec={showSpec} />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
높이는 입력 컨트롤의 외곽 높이입니다. 값·Placeholder 글자는 size를 따르고 Label 14/20/500은 공통입니다. 업무 폼·조회 조합은 Medium을 쓰며, size를 생략하면 Large입니다.
|
||||
</p>
|
||||
{showSpec ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
높이는 입력 컨트롤의 외곽 높이입니다. 값·Placeholder 글자는 size를 따르고 Label 14/20/500은 공통입니다. 업무 폼·조회 조합은 Medium을 쓰며, size를 생략하면 Large입니다.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -118,33 +124,27 @@ const meta = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Representative Story: the common Story page (Overview → Sizes → States → Validation). Focus is placed by the play function.
|
||||
// Representative Story: the common Story page (Sizes → States → Validation). Focus is placed by the play function.
|
||||
// Named after the component, as in ui_viz.
|
||||
export const All: Story = {
|
||||
name: "Input",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Input"
|
||||
description="Label·Helper·Error 문구는 소비 조합"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본값: Large 42px">
|
||||
<StateBoard states={[{ id: "default", label: "Default · size 생략(Large)", children: <Specimen placeholder={placeholder} /> }]} />
|
||||
<StoryPageHeader title="Input" />
|
||||
<StorySection title="Sizes">
|
||||
<SizeComparison showSpec={false} />
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="28 / 34 / 42px · 업무 폼·조회는 Medium">
|
||||
<SizeComparison />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Focus·Error 1.5px 테두리 · Hover 없음 · Disabled 60%">
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "focus", label: "Focus", children: <Specimen size="medium" placeholder={placeholder} /> },
|
||||
{ id: "disabled", label: "Disabled", children: <Specimen disabled defaultValue="Unavailable project" /> },
|
||||
{ id: "readonly", label: "Readonly", note: readonlyNote, noteAfter: true, children: <Specimen readOnly value="Confirmed project" /> },
|
||||
{ id: "readonly", label: "Readonly", children: <Specimen readOnly value="Confirmed project" /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
|
||||
<StorySection title="Validation">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "error", label: "Error", children: <Specimen size="medium" error defaultValue="Invalid value" /> },
|
||||
|
||||
@@ -27,18 +27,13 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Label"
|
||||
description="Field Label 14/20/500 · htmlFor 연결 시 클릭으로 focus 이동"
|
||||
/>
|
||||
<StoryPageHeader title="Label" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
states={[
|
||||
{
|
||||
id: "default",
|
||||
label: "Default · htmlFor",
|
||||
noteAfter: true,
|
||||
note: "라벨을 클릭하면 연결된 입력으로 focus가 이동합니다.",
|
||||
label: "Default",
|
||||
children: (
|
||||
<div className="grid max-w-sm gap-2">
|
||||
<Label htmlFor="label-default">Project name</Label>
|
||||
@@ -54,9 +49,7 @@ export const All: Story = {
|
||||
states={[
|
||||
{
|
||||
id: "disabled",
|
||||
label: "Disabled (peer-disabled, shadcn 기본)",
|
||||
noteAfter: true,
|
||||
note: "Disabled 연동은 ui_viz Label에 정의가 없어 shadcn 기본(peer-disabled)을 유지합니다.",
|
||||
label: "Disabled",
|
||||
children: (
|
||||
<div className="grid max-w-sm gap-2">
|
||||
<Input id="label-disabled" size="medium" disabled defaultValue="Unavailable" className="peer order-2" />
|
||||
@@ -69,7 +62,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="Required 별표는 소비 조합">
|
||||
<StorySection title="Usage">
|
||||
<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>
|
||||
@@ -80,7 +73,7 @@ export const All: Story = {
|
||||
states={[
|
||||
{
|
||||
id: "required",
|
||||
label: "Required (소비 조합)",
|
||||
label: "Required",
|
||||
children: (
|
||||
<div className="grid max-w-sm gap-2">
|
||||
<Label htmlFor="label-required">
|
||||
|
||||
@@ -96,11 +96,8 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Menubar"
|
||||
description="열린 상태에서 좌우 방향키로 이웃 메뉴 이동"
|
||||
/>
|
||||
<StorySection title="Overview" description="단축키 · Disabled · 하위 메뉴 · 위험 항목 · Checkbox · Radio">
|
||||
<StoryPageHeader title="Menubar" />
|
||||
<StorySection title="Overview">
|
||||
<div style={{ minHeight: 384 }}>
|
||||
<Specimen />
|
||||
</div>
|
||||
|
||||
@@ -101,15 +101,12 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Navigation Menu"
|
||||
description="링크 이동용 · 명령 실행에는 쓰지 않음"
|
||||
/>
|
||||
<StoryPageHeader title="Navigation Menu" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={520}
|
||||
states={[
|
||||
{ id: "viewport", label: "Viewport (default)", note: "제품 / 자료는 content, 회사는 현재 위치(active), 문의는 일반 링크, 준비 중은 disabled trigger", children: frame(<Specimen />) },
|
||||
{ id: "viewport", label: "Viewport", children: frame(<Specimen />) },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -117,7 +114,7 @@ export const All: Story = {
|
||||
<StateBoard
|
||||
min={520}
|
||||
states={[
|
||||
{ id: "no-viewport", label: "viewport={false}", note: "각 content가 자기 trigger 아래에 열립니다", children: frame(<Specimen viewport={false} />) },
|
||||
{ id: "no-viewport", label: "No viewport", children: frame(<Specimen viewport={false} />) },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
|
||||
@@ -29,7 +29,8 @@ function pageRange(page: number, total: number): (number | "start" | "end")[] {
|
||||
return out
|
||||
}
|
||||
|
||||
function Specimen({ total, initial }: { total: number; initial: number }) {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Specimen({ total, initial, showValue = true }: { total: number; initial: number; showValue?: boolean }) {
|
||||
const [page, setPage] = useState(initial)
|
||||
const go = (p: number) => (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
@@ -60,7 +61,7 @@ function Specimen({ total, initial }: { total: number; initial: number }) {
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<p className="text-center text-sm text-muted-foreground" data-testid="page-state">{`현재 ${page} / ${total}`}</p>
|
||||
{showValue ? <p className="text-center text-sm text-muted-foreground" data-testid="page-state">{`현재 ${page} / ${total}`}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -68,10 +69,10 @@ function Specimen({ total, initial }: { total: number; initial: number }) {
|
||||
const wrap = (node: ReactNode) => <div className="w-full max-w-xl">{node}</div>
|
||||
|
||||
const sections = [
|
||||
{ id: "first", label: "First page", note: "Previous가 aria-disabled", node: <Specimen total={5} initial={1} /> },
|
||||
{ id: "middle", label: "Middle page", node: <Specimen total={5} initial={3} /> },
|
||||
{ id: "last", label: "Last page", note: "Next가 aria-disabled", node: <Specimen total={5} initial={5} /> },
|
||||
{ id: "ellipsis", label: "Long range with ellipsis", note: "클릭하면 실제로 현재 페이지가 바뀝니다", node: <Specimen total={20} initial={10} /> },
|
||||
{ id: "first", label: "First page", node: <Specimen total={5} initial={1} showValue={false} /> },
|
||||
{ id: "middle", label: "Middle page", node: <Specimen total={5} initial={3} showValue={false} /> },
|
||||
{ id: "last", label: "Last page", node: <Specimen total={5} initial={5} showValue={false} /> },
|
||||
{ id: "ellipsis", label: "Long range", node: <Specimen total={20} initial={10} showValue={false} /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Navigation/Pagination", component: Pagination, parameters: { layout: "padded" } } satisfies Meta<typeof Pagination>
|
||||
@@ -79,7 +80,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={420} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, children: wrap(s.node) }))} />
|
||||
<StateBoard min={420} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: wrap(s.node) }))} />
|
||||
)
|
||||
|
||||
// Representative Story: the common Story page; the sections group the same specimens.
|
||||
@@ -88,12 +89,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Pagination"
|
||||
description="현재 페이지와 href는 소비 코드가 소유"
|
||||
/>
|
||||
<StoryPageHeader title="Pagination" />
|
||||
<StorySection title="Overview">{board("middle")}</StorySection>
|
||||
<StorySection title="States" description="처음·끝에서 이전·다음 aria-disabled">{board("first", "last")}</StorySection>
|
||||
<StorySection title="States">{board("first", "last")}</StorySection>
|
||||
<StorySection title="Usage">{board("ellipsis")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -83,17 +83,14 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Popover"
|
||||
description="비모달 · 바깥 클릭이나 Esc로 닫힘"
|
||||
/>
|
||||
<StoryPageHeader title="Popover" />
|
||||
<StorySection title="Overview">
|
||||
<Info />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<Aligned />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="취소·저장은 PopoverClose로 닫음">
|
||||
<StorySection title="Usage">
|
||||
<FormPanel />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -7,12 +7,13 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
|
||||
// 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.
|
||||
|
||||
function Interactive() {
|
||||
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
||||
function Interactive({ showValue = true }: { showValue?: boolean }) {
|
||||
const [value, setValue] = useState(30)
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Progress value={value} aria-label="진행률" />
|
||||
<p className="text-sm text-muted-foreground" data-testid="value">{`value = ${value}`}</p>
|
||||
{showValue ? <p className="text-sm text-muted-foreground" data-testid="value">{`value = ${value}`}</p> : null}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="cancel" appearance="solid" size="sm" onClick={() => setValue((v) => Math.max(0, v - 25))}>
|
||||
-25
|
||||
@@ -40,8 +41,8 @@ const sections = [
|
||||
{ id: "zero", label: "0%", node: <Progress value={0} aria-label="0%" /> },
|
||||
{ id: "middle", label: "50%", node: <Progress value={50} aria-label="50%" /> },
|
||||
{ id: "full", label: "100%", node: <Progress value={100} aria-label="100%" /> },
|
||||
{ id: "custom-maximum", label: "max = 200", note: "0 / 50 / 200과 값 없음: ARIA 범위와 실제 채움 비율 확인", node: <CustomMaximum /> },
|
||||
{ id: "interactive", label: "Interactive value", note: "버튼으로 value를 바꾸면 indicator 폭과 aria-valuenow가 함께 변합니다", node: <Interactive /> },
|
||||
{ id: "custom-maximum", label: "max = 200", node: <CustomMaximum /> },
|
||||
{ id: "interactive", label: "Interactive", node: <Interactive showValue={false} /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Data & Display/Progress", component: Progress, parameters: { layout: "padded" } } satisfies Meta<typeof Progress>
|
||||
@@ -49,7 +50,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={320} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, noteAfter: true, children: s.node }))} />
|
||||
<StateBoard min={320} states={sections.filter((s) => 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.
|
||||
@@ -58,10 +59,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Progress"
|
||||
description="value(0..max) 비율로 채움"
|
||||
/>
|
||||
<StoryPageHeader title="Progress" />
|
||||
<StorySection title="Overview">{board("middle")}</StorySection>
|
||||
<StorySection title="States">{board("zero", "full")}</StorySection>
|
||||
<StorySection title="Usage">{board("custom-maximum", "interactive")}</StorySection>
|
||||
|
||||
@@ -12,6 +12,7 @@ function Group({
|
||||
disabledItem,
|
||||
invalid,
|
||||
orientation,
|
||||
hoverItem,
|
||||
}: {
|
||||
prefix: string
|
||||
defaultValue?: string
|
||||
@@ -19,12 +20,14 @@ function Group({
|
||||
disabledItem?: string
|
||||
invalid?: boolean
|
||||
orientation?: "horizontal" | "vertical"
|
||||
// Pins :hover on one item with the Storybook-only data-sb-hover attribute (.storybook/pseudo-states.ts).
|
||||
hoverItem?: string
|
||||
}) {
|
||||
return (
|
||||
<RadioGroup defaultValue={defaultValue} disabled={disabled} orientation={orientation} aria-label={prefix}>
|
||||
{["a", "b", "c"].map((v) => (
|
||||
<div key={v} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={v} id={`${prefix}-${v}`} disabled={disabledItem === v} aria-invalid={invalid ? true : undefined} />
|
||||
<RadioGroupItem value={v} id={`${prefix}-${v}`} disabled={disabledItem === v} aria-invalid={invalid ? true : undefined} data-sb-hover={hoverItem === v ? "" : undefined} />
|
||||
<Label htmlFor={`${prefix}-${v}`}>Option {v.toUpperCase()}</Label>
|
||||
</div>
|
||||
))}
|
||||
@@ -53,12 +56,9 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Radio Group"
|
||||
description="그룹 안에서 방향키로 이동·선택"
|
||||
/>
|
||||
<StoryPageHeader title="Radio Group" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard min={240} states={[{ id: "vertical", label: "Vertical (default) · Checked / Unchecked", children: <Group prefix="v" defaultValue="b" /> }]} />
|
||||
<StateBoard min={240} states={[{ id: "vertical", label: "Vertical", children: <Group prefix="v" defaultValue="b" /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<StateBoard min={240} states={[{ id: "horizontal", label: "Horizontal", children: <Group prefix="h" orientation="horizontal" /> }]} />
|
||||
@@ -67,14 +67,14 @@ export const All: Story = {
|
||||
<StateBoard
|
||||
min={240}
|
||||
states={[
|
||||
{ id: "hover", label: "Hover", noteAfter: true, note: "Hover한 항목의 테두리가 primary 색이 됩니다.", children: <Group prefix="hv" defaultValue="" /> },
|
||||
{ id: "focus", label: "Focus", noteAfter: true, note: "그룹에 Tab으로 들어가면 선택된 항목에 focus ring이 표시됩니다.", children: <Group prefix="f" /> },
|
||||
{ id: "hover", label: "Hover", children: <Group prefix="hv" defaultValue="" hoverItem="a" /> },
|
||||
{ id: "focus", label: "Focus", children: <Group prefix="f" /> },
|
||||
{ id: "disabled-item", label: "Disabled · 항목", children: <Group prefix="di" disabledItem="b" /> },
|
||||
{ id: "disabled-group", label: "Disabled · 그룹", children: <Group prefix="dg" disabled /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리">
|
||||
<StorySection title="Validation">
|
||||
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Group prefix="e" invalid /> }]} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -88,10 +88,10 @@ function Collapsible() {
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "horizontal", label: "Horizontal (list min 15%)", node: <Horizontal /> },
|
||||
{ id: "horizontal", label: "Horizontal", node: <Horizontal /> },
|
||||
{ id: "vertical", label: "Vertical with grip", node: <Vertical /> },
|
||||
{ id: "handle", label: "Horizontal with grip", node: <WithHandle /> },
|
||||
{ id: "minmax", label: "Min / max (25%–60%)", node: <MinMax /> },
|
||||
{ id: "minmax", label: "Min / max", node: <MinMax /> },
|
||||
{ id: "collapsible", label: "Collapsible panel", node: <Collapsible /> },
|
||||
]
|
||||
|
||||
@@ -114,7 +114,7 @@ export const All: Story = {
|
||||
/>
|
||||
<StorySection title="Overview">{board("horizontal")}</StorySection>
|
||||
<StorySection title="Variants">{board("vertical", "handle")}</StorySection>
|
||||
<StorySection title="Usage" description="최소·최대 크기 · 접기">{board("minmax", "collapsible")}</StorySection>
|
||||
<StorySection title="Usage">{board("minmax", "collapsible")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -87,10 +87,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Scroll Area"
|
||||
description="스크롤바는 hover·스크롤 시 표시"
|
||||
/>
|
||||
<StoryPageHeader title="Scroll Area" />
|
||||
<StorySection title="Overview">{board("vertical")}</StorySection>
|
||||
<StorySection title="Variants">{board("horizontal", "both")}</StorySection>
|
||||
<StorySection title="Usage">{board("table-like")}</StorySection>
|
||||
|
||||
@@ -71,35 +71,40 @@ const sizes = [
|
||||
{ size: "large" as const, name: "Large", outer: "44px / 42px", typography: "16 / 24 / 400", option: "42px minimum · 16 / 24 / 400", usage: "Component default" },
|
||||
]
|
||||
|
||||
function SizeCard({ spec }: { spec: (typeof sizes)[number] }) {
|
||||
function SizeCard({ spec, showSpec = true }: { spec: (typeof sizes)[number]; showSpec?: boolean }) {
|
||||
return (
|
||||
<section className="grid content-start gap-4" data-select-size={spec.size}>
|
||||
<SelectSpecimen size={spec.size} label={spec.name} placeholder="Choose a status" />
|
||||
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-muted-foreground">Outer Height / Trigger</dt>
|
||||
<dd className="m-0">{spec.outer}</dd>
|
||||
<dt className="text-muted-foreground">Trigger Typography</dt>
|
||||
<dd className="m-0">{spec.typography}</dd>
|
||||
<dt className="text-muted-foreground">Option</dt>
|
||||
<dd className="m-0">{spec.option}</dd>
|
||||
<dt className="text-muted-foreground">Usage</dt>
|
||||
<dd className="m-0">{spec.usage}</dd>
|
||||
</dl>
|
||||
{showSpec ? (
|
||||
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
<dt className="text-muted-foreground">Outer Height / Trigger</dt>
|
||||
<dd className="m-0">{spec.outer}</dd>
|
||||
<dt className="text-muted-foreground">Trigger Typography</dt>
|
||||
<dd className="m-0">{spec.typography}</dd>
|
||||
<dt className="text-muted-foreground">Option</dt>
|
||||
<dd className="m-0">{spec.option}</dd>
|
||||
<dt className="text-muted-foreground">Usage</dt>
|
||||
<dd className="m-0">{spec.usage}</dd>
|
||||
</dl>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function SizeComparison() {
|
||||
// `showSpec={false}` keeps the representative page UI-only; the verification Story keeps the spec lists and the size note.
|
||||
function SizeComparison({ showSpec = true }: { showSpec?: boolean }) {
|
||||
return (
|
||||
<div className="grid max-w-[1120px] gap-6">
|
||||
<div className="grid gap-6 [grid-template-columns:repeat(auto-fit,minmax(240px,1fr))]">
|
||||
{sizes.map((spec) => (
|
||||
<SizeCard key={spec.size} spec={spec} />
|
||||
<SizeCard key={spec.size} spec={spec} showSpec={showSpec} />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select는 Input과 독립된 두 크기(Medium 34 / Large 44)입니다. 각 Trigger를 클릭해 연결된 목록과 선택 상태를 확인하세요. 목록의 Option은 Trigger의 size를 그대로 따르며(최소 높이 42px, Medium 14/20/400, Large 16/24/400) 폭은 항상 Trigger와 같습니다.
|
||||
</p>
|
||||
{showSpec ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select는 Input과 독립된 두 크기(Medium 34 / Large 44)입니다. 각 Trigger를 클릭해 연결된 목록과 선택 상태를 확인하세요. 목록의 Option은 Trigger의 size를 그대로 따르며(최소 높이 42px, Medium 14/20/400, Large 16/24/400) 폭은 항상 Trigger와 같습니다.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -144,52 +149,32 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Representative Story: every state on one page. Named after the component, as in ui_viz.
|
||||
// The open list is a Radix popover: press Escape or click outside to close it, click the trigger to reopen.
|
||||
// It starts at Sizes; each trigger opens the real list (the open-on-load list is the hidden `Open` Story).
|
||||
export const All: Story = {
|
||||
name: "Select",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Select"
|
||||
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="기본값: Large 44px · 열린 목록">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
{ id: "default", label: "Default · Large (size 생략)", children: <SelectSpecimen /> },
|
||||
{
|
||||
id: "open",
|
||||
label: "Open",
|
||||
children: (
|
||||
<div className="min-h-64">
|
||||
<SelectSpecimen size="medium" defaultOpen />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="34 / 44px · Input과 별도 크기">
|
||||
<SizeComparison />
|
||||
<StoryPageHeader title="Select" />
|
||||
<StorySection title="Sizes">
|
||||
<SizeComparison showSpec={false} />
|
||||
</StorySection>
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
{ id: "selected", label: "Selected", children: <SelectSpecimen size="medium" defaultValue="active" /> },
|
||||
{ id: "disabled", label: "Disabled", children: <DisabledStates /> },
|
||||
{ id: "selected", label: "Selected", children: <SelectSpecimen size="medium" label="Status" defaultValue="active" /> },
|
||||
{ id: "disabled", label: "Disabled", children: <SelectSpecimen size="medium" label="Status" defaultValue="active" disabled /> },
|
||||
{ id: "disabled-placeholder", label: "Disabled · Placeholder", children: <SelectSpecimen size="medium" label="Status" disabled /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Invalid / Focus" description="aria-invalid → Error 테두리">
|
||||
<StorySection title="Invalid / Focus">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
{ id: "invalid", label: "Invalid", children: <SelectSpecimen size="medium" aria-invalid label="Invalid Select" defaultValue="active" /> },
|
||||
{ id: "invalid-disabled", label: "Invalid · Disabled", children: <SelectSpecimen size="medium" aria-invalid disabled label="Disabled Invalid Select" /> },
|
||||
{ id: "invalid", label: "Invalid", children: <SelectSpecimen size="medium" aria-invalid label="Status" defaultValue="active" /> },
|
||||
{ id: "invalid-disabled", label: "Invalid · Disabled", children: <SelectSpecimen size="medium" aria-invalid disabled label="Status" /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
|
||||
@@ -25,10 +25,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Separator"
|
||||
description="구분선은 묶음 뒤 한 곳 · 기본 decorative"
|
||||
/>
|
||||
<StoryPageHeader title="Separator" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={280}
|
||||
@@ -53,7 +50,7 @@ export const All: Story = {
|
||||
states={[
|
||||
{
|
||||
id: "vertical",
|
||||
label: "Vertical (flex row)",
|
||||
label: "Vertical",
|
||||
children: (
|
||||
<div className="flex h-6 items-center gap-3 text-sm">
|
||||
<span>Left</span>
|
||||
@@ -66,9 +63,7 @@ export const All: Story = {
|
||||
},
|
||||
{
|
||||
id: "semantic",
|
||||
label: "Semantic (decorative=false)",
|
||||
noteAfter: true,
|
||||
note: "decorative=false는 role=separator와 aria-orientation을 노출합니다.",
|
||||
label: "Semantic",
|
||||
children: (
|
||||
<div className="grid gap-3 text-sm">
|
||||
<span>Section A</span>
|
||||
|
||||
@@ -101,7 +101,7 @@ export const All: Story = {
|
||||
<StoryPageHeader
|
||||
title="Sheet"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본값: right">
|
||||
<StorySection title="Overview">
|
||||
<SideCase side="right" />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
@@ -111,7 +111,7 @@ export const All: Story = {
|
||||
<SideCase side="bottom" />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
|
||||
<StorySection title="Usage">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<FormCase />
|
||||
<LongCase />
|
||||
|
||||
@@ -63,10 +63,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Skeleton"
|
||||
description="크기·모양은 className으로 지정"
|
||||
/>
|
||||
<StoryPageHeader title="Skeleton" />
|
||||
<StorySection title="Overview">{board("text")}</StorySection>
|
||||
<StorySection title="Usage">{board("avatar", "card")}</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -61,7 +61,7 @@ function Vertical() {
|
||||
|
||||
const sections = [
|
||||
{ id: "single", label: "Single", node: <Single /> },
|
||||
{ id: "range", label: "Range (two thumbs)", node: <Range /> },
|
||||
{ id: "range", label: "Range", node: <Range /> },
|
||||
{ id: "step", label: "Step", node: <Step /> },
|
||||
{ id: "disabled", label: "Disabled", node: <Disabled /> },
|
||||
{ id: "vertical", label: "Vertical", node: <Vertical /> },
|
||||
@@ -81,10 +81,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Slider"
|
||||
description="끌기 또는 화살표 키로 값 변경"
|
||||
/>
|
||||
<StoryPageHeader title="Slider" />
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Variants">{board("range", "step", "vertical")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
|
||||
@@ -27,8 +27,6 @@ function Board({ ids }: { ids: string[] }) {
|
||||
{
|
||||
id: "types",
|
||||
label: "Types",
|
||||
noteAfter: true,
|
||||
note: "Default / Success / Error / Warning / Info / Loading. Only Error has its own color; the other types are neutral (status colors are undecided).",
|
||||
children: (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Trigger onClick={() => toast("알림이 도착했습니다", { description: "기본 알림입니다." })}>Default</Trigger>
|
||||
@@ -67,8 +65,6 @@ function Board({ ids }: { ids: string[] }) {
|
||||
{
|
||||
id: "promise",
|
||||
label: "Promise",
|
||||
noteAfter: true,
|
||||
note: "Loading, then Success or Error (local timeout, no network).",
|
||||
children: (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Trigger onClick={() => toast.promise(wait(1500), { loading: "저장 중...", success: "저장했습니다", error: "저장하지 못했습니다" })}>
|
||||
@@ -124,10 +120,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Sonner"
|
||||
description="Toaster는 앱 루트에 한 번"
|
||||
/>
|
||||
<StoryPageHeader title="Sonner" />
|
||||
<StorySection title="Variants">
|
||||
<Board ids={["types"]} />
|
||||
</StorySection>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useId, useState, type ReactNode } from "react"
|
||||
import { useId, 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"
|
||||
@@ -14,11 +14,6 @@ function Option({ label, children }: { label: string; children: (id: string) =>
|
||||
)
|
||||
}
|
||||
|
||||
function Controlled() {
|
||||
const [on, setOn] = useState(false)
|
||||
return <Option label={`Notifications (${on ? "on" : "off"})`}>{(id) => <Switch id={id} checked={on} onCheckedChange={setOn} />}</Option>
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/Switch",
|
||||
component: Switch,
|
||||
@@ -40,17 +35,13 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Switch"
|
||||
description="트랙 36×20 · 썸 16 · On은 state-active 색"
|
||||
/>
|
||||
<StoryPageHeader title="Switch" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={240}
|
||||
states={[
|
||||
{ id: "off", label: "Off", children: <Option label="Option">{(id) => <Switch id={id} />}</Option> },
|
||||
{ id: "on", label: "On", children: <Option label="Option">{(id) => <Switch id={id} defaultChecked />}</Option> },
|
||||
{ id: "interactive", label: "Controlled · click toggles", children: <Controlled /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -58,7 +49,17 @@ export const All: Story = {
|
||||
<StateBoard
|
||||
min={240}
|
||||
states={[
|
||||
{ id: "hover", label: "Hover", noteAfter: true, note: "Black 20% overlay. 포인터를 올려 확인합니다.", children: <Option label="Option">{(id) => <Switch id={id} />}</Option> },
|
||||
{
|
||||
id: "hover",
|
||||
label: "Hover",
|
||||
// data-sb-hover pins :hover (Storybook only, .storybook/pseudo-states.ts).
|
||||
children: (
|
||||
<div className="grid gap-3">
|
||||
<Option label="Off">{(id) => <Switch id={id} data-sb-hover="" />}</Option>
|
||||
<Option label="On">{(id) => <Switch id={id} defaultChecked data-sb-hover="" />}</Option>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ id: "focus", label: "Focus", children: <Option label="Option">{(id) => <Switch id={id} />}</Option> },
|
||||
{
|
||||
id: "disabled",
|
||||
|
||||
@@ -15,9 +15,11 @@ const projects = [
|
||||
]
|
||||
const won = (n: number) => n.toLocaleString("ko-KR")
|
||||
|
||||
function BasicTable() {
|
||||
// `showCaption={false}` drops the visible caption on the representative page; the caption text becomes the table's
|
||||
// accessible name. The verification Story keeps the TableCaption.
|
||||
function BasicTable({ showCaption = true }: { showCaption?: boolean }) {
|
||||
return (
|
||||
<Table>
|
||||
<Table aria-label={showCaption ? undefined : "프로젝트별 계약 금액(원)"}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>번호</TableHead>
|
||||
@@ -44,7 +46,7 @@ function BasicTable() {
|
||||
<TableCell className="text-right">{won(projects.reduce((s, p) => s + p.amount, 0))}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
<TableCaption>프로젝트별 계약 금액(원)</TableCaption>
|
||||
{showCaption ? <TableCaption>프로젝트별 계약 금액(원)</TableCaption> : null}
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -132,10 +134,10 @@ function WideTable() {
|
||||
const box = (node: ReactNode) => <div style={{ width: "100%", maxWidth: 720, minWidth: 0 }}>{node}</div>
|
||||
|
||||
const sections = [
|
||||
{ id: "basic", label: "Basic + Badge + numeric + footer + caption", node: <BasicTable /> },
|
||||
{ id: "selected", label: "Selected row", note: "Checkbox와 data-state=\"selected\"는 Story가 조합합니다(Table에는 선택 로직이 없음)", node: <SelectableTable /> },
|
||||
{ id: "basic", label: "Basic", node: <BasicTable showCaption={false} /> },
|
||||
{ id: "selected", label: "Selected row", node: <SelectableTable /> },
|
||||
{ id: "long", label: "Long text", node: <LongTextTable /> },
|
||||
{ id: "wide", label: "Horizontal overflow", note: "container가 가로로 scroll 하고 페이지는 밀리지 않습니다", node: <WideTable /> },
|
||||
{ id: "wide", label: "Horizontal overflow", node: <WideTable /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Data & Display/Table", component: Table, parameters: { layout: "padded" } } satisfies Meta<typeof Table>
|
||||
@@ -143,7 +145,7 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={460} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, note: s.note, wide: true, children: box(s.node) }))} />
|
||||
<StateBoard min={460} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, wide: true, children: box(s.node) }))} />
|
||||
)
|
||||
|
||||
// Representative Story: the common Story page; the sections group the same specimens.
|
||||
@@ -152,10 +154,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Table"
|
||||
description="정적 표시 · 정렬·선택·페이지는 Data Table Pattern"
|
||||
/>
|
||||
<StoryPageHeader title="Table" />
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="States">{board("selected")}</StorySection>
|
||||
<StorySection title="Usage">{board("long", "wide")}</StorySection>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { useId, useState, type ComponentProps } from "react"
|
||||
import {
|
||||
Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea,
|
||||
Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
@@ -86,8 +86,7 @@ function SurfaceUsage({ inCard = false }: { inCard?: boolean }) {
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="schedule" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>프로젝트 산출물 일정 예시</TableCaption>
|
||||
<Table aria-label="프로젝트 산출물 일정">
|
||||
<TableHeader><TableRow><TableHead scope="col">산출물</TableHead><TableHead scope="col">마감일</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>화면 설계서</TableCell><TableCell>10월 14일</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
@@ -133,7 +132,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"기본 TabsList는 Primary(fill=true, panelSurface=\"content\")입니다. 대표 Story는 기본 API와 명시적 Primary, Secondary(fill + panelSurface=\"surface\"), Primary Overflow를 비교합니다. 모든 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는 기본 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 이력은 유지합니다.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -146,49 +145,50 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Tabs"
|
||||
description="선택 탭은 콘텐츠 면과 이어짐 · 넘칠 때만 이전/다음 버튼"
|
||||
/>
|
||||
<StoryPageHeader title="Tabs" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary · 비선택 Hover Black 20%", children: <Specimen /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Default API">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "primary-fill", label: "Primary · TabsList fill", noteAfter: true, note: "panelSurface 생략 → content. 기본 TabsList와 같은 Primary입니다.", children: <Specimen listProps={{ fill: true }} /> },
|
||||
{ id: "primary-explicit", label: "Primary · fill + panelSurface content", noteAfter: true, note: "두 Props를 명시해도 기본 API와 같은 Primary입니다.", children: <Specimen listProps={{ fill: true, panelSurface: "content" }} /> },
|
||||
{ id: "primary", label: "Primary", children: <Specimen /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "secondary", label: "Secondary · Fill + panelSurface surface", noteAfter: true, note: "서브형 · Surface(White) 위에서 사용", children: <Specimen variant="secondary" /> },
|
||||
{ id: "secondary", label: "Secondary", 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", children: <div className="w-full min-w-0 max-w-4xl"><Specimen count={20} /></div> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="panelSurface를 상위 표면에 맞춤 · px-0 py-6은 예시이며 공통 규칙 아님">
|
||||
<StorySection title="Usage">
|
||||
<StateBoard states={[
|
||||
{ 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 /> },
|
||||
{ id: "usage-card", label: "Card 내부 · Secondary", children: <SurfaceUsage inCard /> },
|
||||
{ id: "usage-page", label: "페이지 직접 배치 · Primary", children: <SurfaceUsage /> },
|
||||
]} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// Verification: `fill`만, `fill` + `panelSurface="content"`를 명시해도 기본 Primary와 같다(대표 페이지의 이전 Default API 구역).
|
||||
export const DefaultApi: Story = {
|
||||
tags: ["verification"],
|
||||
name: "Default API",
|
||||
render: () => (
|
||||
<div className="grid max-w-md gap-6">
|
||||
<Specimen listProps={{ fill: true }} />
|
||||
<Specimen listProps={{ fill: true, panelSurface: "content" }} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
// Verification: Primary 기준 상태와 Secondary 비교. Secondary도 같은 CSS 경로를 쓴다.
|
||||
export const Secondary: Story = { tags: ["verification"], render: () => <div className="max-w-md"><Specimen variant="secondary" /></div> }
|
||||
export const SelectedFirst: Story = { tags: ["verification"], name: "Selected First", render: () => <div className="max-w-md"><Specimen defaultValue="menu1" /></div> }
|
||||
|
||||
@@ -53,7 +53,6 @@ const narrow = (children: ReactNode) => <div className="max-w-sm">{children}</di
|
||||
const placeholder = "Enter a description"
|
||||
const multiline = "First line\nSecond line keeps its line break.\nThird line."
|
||||
|
||||
const hoverNote = "Hover는 Default와 같습니다(사용자 결정 TEXTAREA-05). 포인터를 올려도 면, 테두리, 글자색이 바뀌지 않습니다."
|
||||
const readonlyNote =
|
||||
"Readonly는 Default의 면, 테두리, typography를 유지하고 흐리게 표시하지 않습니다. 값 변경은 막고 선택·복사는 허용하며 resize 표시가 없습니다."
|
||||
|
||||
@@ -86,24 +85,20 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Textarea"
|
||||
description="최소 높이 112px · resize vertical · size 없음 · Label·Helper·Error는 소비 조합"
|
||||
/>
|
||||
<StoryPageHeader title="Textarea" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard states={[{ id: "default", label: "Default", children: <Specimen placeholder={placeholder} /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Focus 1.5px 테두리(shadow 없음) · Hover = Default">
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "hover", label: "Hover (= Default)", note: hoverNote, noteAfter: true, children: <Specimen defaultValue="Hover over this field" /> },
|
||||
{ id: "focus", label: "Focus", children: <Specimen placeholder={placeholder} /> },
|
||||
{ id: "readonly", label: "Readonly", note: readonlyNote, noteAfter: true, children: <Specimen readOnly defaultValue="Confirmed description" /> },
|
||||
{ id: "readonly", label: "Readonly", children: <Specimen readOnly defaultValue="Confirmed description" /> },
|
||||
{ id: "disabled", label: "Disabled", children: <Specimen disabled defaultValue="Unavailable description" /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
|
||||
<StorySection title="Validation">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "error", label: "Error", children: <Specimen error defaultValue="Invalid value" /> },
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useState } from "react"
|
||||
import { useState, type ReactNode } from "react"
|
||||
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 { 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
|
||||
// Stories tagged `verification` (hidden from the sidebar). Icon-only items carry an `aria-label`.
|
||||
|
||||
function Single() {
|
||||
// `showValue={false}` hides the selected-value readout on the representative page; the verification Stories keep it.
|
||||
function Single({ showValue = true }: { showValue?: boolean }) {
|
||||
const [value, setValue] = useState("left")
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
@@ -18,12 +18,12 @@ function Single() {
|
||||
<ToggleGroupItem value="center">가운데</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right">오른쪽</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<span className="text-sm text-muted-foreground" data-testid="single">{`value = ${value}`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="single">{`value = ${value}`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Multiple() {
|
||||
function Multiple({ showValue = true }: { showValue?: boolean }) {
|
||||
const [value, setValue] = useState<string[]>(["bold"])
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
@@ -38,7 +38,7 @@ function Multiple() {
|
||||
<UnderlineIcon />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<span className="text-sm text-muted-foreground" data-testid="multiple">{`value = [${value.join(", ")}]`}</span>
|
||||
{showValue ? <span className="text-sm text-muted-foreground" data-testid="multiple">{`value = [${value.join(", ")}]`}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -111,23 +111,18 @@ function Sizes() {
|
||||
)
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "single", label: "Single selection", node: <Single /> },
|
||||
{ id: "multiple", label: "Multiple selection (icon-only)", node: <Multiple /> },
|
||||
{ id: "sizes", label: "Sizes · Icon-only / Icon + Text / Text-only", node: <Sizes /> },
|
||||
{ id: "disabled", label: "Disabled group / item", node: <Disabled /> },
|
||||
{ id: "vertical", label: "Vertical", node: <Vertical /> },
|
||||
{ id: "spaced", label: "spacing={2} (separated)", node: <Spaced /> },
|
||||
{ id: "many", label: "Many items (spacing + flex-wrap, consumer opts in)", node: <Many /> },
|
||||
]
|
||||
|
||||
// No `component`: ToggleGroup props are a union (single | multiple), so Stories without args would not type-check.
|
||||
const meta = { title: "Components/Form/Toggle Group", parameters: { layout: "padded" } } satisfies Meta
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const board = (...ids: string[]) => (
|
||||
<StateBoard min={360} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: s.node }))} />
|
||||
// The representative page shows the groups without example titles: the section title names them.
|
||||
const examples = (...nodes: ReactNode[]) => (
|
||||
<div className="flex flex-wrap items-start gap-x-10 gap-y-6">
|
||||
{nodes.map((node, i) => (
|
||||
<div key={i}>{node}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Representative Story: the common Story page; the sections group the same specimens.
|
||||
@@ -136,17 +131,14 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Toggle Group"
|
||||
description="툴바형 눌림 항목 묶음 · 내용 전환은 Tabs 사용"
|
||||
/>
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StoryPageHeader title="Toggle Group" />
|
||||
<StorySection title="Overview">{examples(<Single showValue={false} />)}</StorySection>
|
||||
<StorySection title="Sizes">
|
||||
<Sizes />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">{board("multiple", "vertical", "spaced")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
<StorySection title="Usage">{board("many")}</StorySection>
|
||||
<StorySection title="Variants">{examples(<Multiple showValue={false} />, <Vertical />, <Spaced />)}</StorySection>
|
||||
<StorySection title="States">{examples(<Disabled />)}</StorySection>
|
||||
<StorySection title="Usage">{examples(<div className="max-w-sm"><Many /></div>)}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
|
||||
import { Toggle, ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
||||
@@ -63,34 +62,9 @@ function Disabled() {
|
||||
)
|
||||
}
|
||||
|
||||
function Controlled() {
|
||||
const [pressed, setPressed] = useState(false)
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Toggle variant="outline" pressed={pressed} onPressedChange={setPressed}>
|
||||
알림
|
||||
</Toggle>
|
||||
<span className="text-sm text-muted-foreground" data-testid="controlled">{`pressed = ${String(pressed)}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// className extension: the consumer's width / rounded utilities win over the default look.
|
||||
function Extended() {
|
||||
return (
|
||||
<Toggle variant="outline" className="w-40 rounded-full">
|
||||
w-40 rounded-full
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "offon", label: "Off / On", node: <OffOn /> },
|
||||
{ id: "icon", label: "Icon-only and icon + text", node: <WithIcon /> },
|
||||
{ id: "sizes", label: "Sizes", node: <Sizes /> },
|
||||
{ id: "disabled", label: "Disabled", node: <Disabled /> },
|
||||
{ id: "controlled", label: "Controlled", node: <Controlled /> },
|
||||
{ id: "extended", label: "className extension", node: <Extended /> },
|
||||
]
|
||||
|
||||
const meta = { title: "Components/Form/Toggle", component: Toggle, parameters: { layout: "padded" } } satisfies Meta<typeof Toggle>
|
||||
@@ -107,24 +81,22 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Toggle"
|
||||
description="Toggle Group과 같은 눌림(On) 상태 기준"
|
||||
/>
|
||||
<StoryPageHeader title="Toggle" />
|
||||
<StorySection title="Overview">{board("offon")}</StorySection>
|
||||
<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>
|
||||
<div><p className="mb-2">연결 그룹 · 다중 선택</p><ToggleGroup type="multiple" variant="outline" defaultValue={["bold"]} aria-label="글자 스타일"><ToggleGroupItem value="bold">굵게</ToggleGroupItem><ToggleGroupItem value="italic">기울임</ToggleGroupItem></ToggleGroup></div>
|
||||
</div>
|
||||
<StorySection title="단독 · 연결 그룹">
|
||||
<StateBoard
|
||||
min={360}
|
||||
states={[
|
||||
{ id: "single-toggle", label: "단독", children: <Toggle variant="outline">알림</Toggle> },
|
||||
{ id: "group-single", label: "연결 그룹 · 단일 선택", children: <ToggleGroup type="single" variant="outline" defaultValue="left" aria-label="정렬"><ToggleGroupItem value="left">왼쪽</ToggleGroupItem><ToggleGroupItem value="center">가운데</ToggleGroupItem><ToggleGroupItem value="right">오른쪽</ToggleGroupItem></ToggleGroup> },
|
||||
{ id: "group-multiple", label: "연결 그룹 · 다중 선택", children: <ToggleGroup type="multiple" variant="outline" defaultValue={["bold"]} aria-label="글자 스타일"><ToggleGroupItem value="bold">굵게</ToggleGroupItem><ToggleGroupItem value="italic">기울임</ToggleGroupItem></ToggleGroup> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes">
|
||||
<Sizes />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">{board("icon")}</StorySection>
|
||||
<StorySection title="States">{board("disabled", "controlled")}</StorySection>
|
||||
<StorySection title="Usage">{board("extended")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -76,17 +76,14 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<TooltipProvider>
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Tooltip"
|
||||
description="상호작용 없는 짧은 설명 · hover 또는 focus로 열림"
|
||||
/>
|
||||
<StoryPageHeader title="Tooltip" />
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<Placements />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="Disabled 버튼은 감싼 요소를 Trigger로 사용">
|
||||
<StorySection title="Usage">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<LongText />
|
||||
<Disabled />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StrictMode, useMemo, useState } from 'react';
|
||||
import { StrictMode, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { OrgChart } from '@hm-design/org-chart-react/hm-dialog';
|
||||
import {
|
||||
@@ -21,22 +21,40 @@ const topNodes = { ptc: { title: '기술총괄', name: '가상책임자 5' } };
|
||||
const layout: OrgChartAutoLayoutOptions = { type: 'tb', hybridStartDepth: 2, initialVisibleDepth: 'all' };
|
||||
const interactions: OrgChartInteractionOptions = { collapse: true, zoom: true, pan: true };
|
||||
|
||||
// Story-only: hides Core's explanatory toolbar text (as-of date, selection count, Drag & Drop guide) on screen.
|
||||
// The elements stay in the shadow DOM: the move feedback remains the cards' aria-describedby / live status
|
||||
// (visually hidden), and the OrgChart browser checks still read their text.
|
||||
const previewText = new CSSStyleSheet();
|
||||
previewText.replaceSync(`
|
||||
.hm-orgchart-date, .hm-orgchart-expanded-selection { display: none; }
|
||||
.hm-orgchart-move-feedback { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
||||
`);
|
||||
function hidePreviewText(container: HTMLElement): () => void {
|
||||
const adopt = () => {
|
||||
const shadow = container.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
if (shadow && !shadow.adoptedStyleSheets.includes(previewText)) shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, previewText];
|
||||
};
|
||||
const observer = new MutationObserver(adopt);
|
||||
observer.observe(container, { childList: true, subtree: true });
|
||||
adopt();
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
/** Existing Expanded Story only: a chart preview without setting panels. */
|
||||
function ExpandedPreview() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const preview = useRef<HTMLDivElement>(null);
|
||||
useLayoutEffect(() => hidePreviewText(preview.current!), []);
|
||||
const source = useMemo(() => new JsonOrgChartDataSource(createOrgChartExampleDataset(template, {
|
||||
scope: 'per-company', employeeCount: 20, companyIds: allCompanies,
|
||||
})), []);
|
||||
|
||||
return <StoryPage className="hm-orgchart-story-page hm-orgchart-story-expanded">
|
||||
<StoryPageHeader
|
||||
title="Expanded OrgChart"
|
||||
description="모든 직원은 가상 데이터"
|
||||
/>
|
||||
<StoryPageHeader title="Expanded OrgChart" />
|
||||
{/* Preview first: the chart sits right under the page title; its own header already names it, so no section header. */}
|
||||
<div className="hm-orgchart-story hm-orgchart-story-lead">
|
||||
{error ? <p role="alert" className="hm-orgchart-story-note">{error}</p> : null}
|
||||
<div className="hm-orgchart-story-preview sb-center">
|
||||
<div ref={preview} className="hm-orgchart-story-preview sb-center">
|
||||
<OrgChart dataSource={source} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" autoLayout={layout} interactions={interactions} dragDropEnabled={false} selectedCompanyIds={allCompanies} companyTopNodes={topNodes} employeePopup="hm-dialog" className="hm-orgchart-story-chart" onLifecycleError={cause => setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ export function CompositionPreview() {
|
||||
const host = useRef<HTMLElement>(null);
|
||||
useLayoutEffect(() => centerCompositionPreview(host.current!), []);
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader title="Full Composition" description="Workdesk 기반 고정형 조직도" />
|
||||
<StorySection title="고정형 조직도" description="직원 카드를 눌러 정보 확인 · 미리보기 폭에 맞춰 비례 축소">
|
||||
<StoryPageHeader title="Full Composition" />
|
||||
<StorySection title="고정형 조직도">
|
||||
<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>
|
||||
@@ -39,14 +39,14 @@ export function DepartmentTypographyPreview() {
|
||||
|
||||
export function OverviewPreview() {
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader title="Overview" description="회사 로고와 인원 현황 · 가상 데이터" />
|
||||
<StorySection title="관계사·회사 로고" description="회사 로고와 기준 안내">
|
||||
<StoryPageHeader title="Overview" />
|
||||
<StorySection title="관계사·회사 로고">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="Logo" hideLogoHeading /></div>
|
||||
</StorySection>
|
||||
<StorySection title="조직·직위별 인원 현황" description="가상 데이터 조직·직위별 집계">
|
||||
<StorySection title="조직·직위별 인원 현황">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="PositionSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
<StorySection title="관계사별 인원 현황" description="관계사별 · 실제 운영 인원수 아님">
|
||||
<StorySection title="관계사별 인원 현황">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="AffiliationSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
|
||||
@@ -430,14 +430,8 @@ const frame = (height?: number): CSSProperties | undefined => (height ? ({ "--hm
|
||||
export function DashboardAll() {
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Dashboard"
|
||||
description="DASHBOARD 단일 모드 · 48px Compact Header + 상태 필터 + 콘텐츠"
|
||||
/>
|
||||
<StorySection
|
||||
title="Overview"
|
||||
description="최소 폭 960px · 좁으면 가로 스크롤"
|
||||
>
|
||||
<StoryPageHeader title="Dashboard" />
|
||||
<StorySection title="Overview">
|
||||
<DashboardScreen mode="compact" />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
Card, CardContent, PageHeader, PageHeaderContent, PageHeaderTitle, PageHeaderDescription,
|
||||
Card, CardContent, CardHeader, CardTitle,
|
||||
Badge,
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -118,9 +118,10 @@ function WithFilterBar({ extended = true }: { extended?: boolean }) {
|
||||
setSearch(""); setStatus("all"); setDate(undefined); setDepartment(undefined)
|
||||
table.resetGlobalFilter(true); table.resetColumnFilters(true)
|
||||
}
|
||||
return <div className="grid min-w-0 gap-4">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>사용자 목록</PageHeaderTitle><PageHeaderDescription>조건을 입력한 뒤 조회합니다.</PageHeaderDescription></PageHeaderContent></PageHeader>
|
||||
<Card className="min-w-0"><CardContent>
|
||||
// The list title belongs to the list: Card title → search → table inside the one white Card.
|
||||
return <Card className="min-w-0">
|
||||
<CardHeader><CardTitle>사용자 목록</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid min-w-0 gap-4">
|
||||
<div className="hm-data-table-tools">
|
||||
<form className="hm-data-table-search" onSubmit={event => { event.preventDefault(); apply() }}>
|
||||
@@ -139,8 +140,8 @@ function WithFilterBar({ extended = true }: { extended?: boolean }) {
|
||||
</div>
|
||||
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
||||
</div>
|
||||
</CardContent></Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
|
||||
const meta = { title: "Patterns/Data/Data Table", parameters: { layout: "padded", controls: { disable: true } } } satisfies Meta
|
||||
@@ -153,15 +154,12 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Data Table"
|
||||
description="선택 · 정렬 · 열 표시 · 페이지 이동"
|
||||
/>
|
||||
<StoryPageHeader title="Data Table" />
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="기본 목록 검색" description="8px 간격 가운데 정렬 · 열 버튼은 공간 부족 시만 다음 행 · 제목은 Card 밖"><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색" description="부서·등록일 구성은 왼쪽 정렬 · 모바일 버튼 행은 우측 정렬"><WithFilterBar /></StorySection>
|
||||
<StorySection title="기본 목록 검색"><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색"><WithFilterBar /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ const categories = [
|
||||
{ value: "safety", label: "안전" },
|
||||
]
|
||||
|
||||
function Full() {
|
||||
// `showStatus` is for the hidden DraftApplied Story only: scripts/verify-quality-reference.mjs reads the
|
||||
// input / applied counts there. The representative page shows the controls without the readout.
|
||||
function Full({ showStatus = false }: { showStatus?: boolean }) {
|
||||
const searchId = useId()
|
||||
const [search, setSearch] = useState("")
|
||||
const [status, setStatus] = useState("all")
|
||||
@@ -67,9 +69,11 @@ function Full() {
|
||||
</Button>
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
<p className="text-sm text-muted-foreground" role="status">
|
||||
입력한 조건 {active}개 · 조회 적용 조건 {applied}개
|
||||
</p>
|
||||
{showStatus ? (
|
||||
<p className="text-sm text-muted-foreground" role="status">
|
||||
입력한 조건 {active}개 · 조회 적용 조건 {applied}개
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -79,24 +83,19 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Representative Story: search, Select, Combobox, Date Picker and the actions row. Narrow it to 390px to see the wrapping.
|
||||
// The Usage / Do & Don't rules live in docs/patterns/filter-bar.md.
|
||||
export const All: Story = {
|
||||
name: "Filter Bar",
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Filter Bar"
|
||||
description="HM 컨트롤을 children으로 조합 · 값은 소비 코드 소유 · Data Table 비의존"
|
||||
/>
|
||||
<StorySection title="Overview" description="좁은 폭에서 줄바꿈">
|
||||
<StoryPageHeader title="Filter Bar" />
|
||||
<StorySection title="Overview">
|
||||
<Full />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="list-extended / list-basic · 390px에서 검색·액션 줄바꿈">
|
||||
<p>입력은 draft로 유지하고 조회에만 적용합니다. 초기화는 입력과 적용값을 함께 비웁니다. 목록은 PageHeader 밖의 흰 Card 하나에 검색과 결과를 함께 배치합니다.</p>
|
||||
</StorySection>
|
||||
<StorySection title="Do & Don't" description="요구한 조건과 액션만 구성합니다.">
|
||||
<p>Do: 공개 Label과 control ID 연결, Solid Green 조회와 Outline Gray 초기화. Don't: Story에 보이는 분류·날짜·메뉴·CSV를 요구사항 없이 추가하거나 임의 토큰으로 색을 지정하지 않습니다.</p>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// Draft vs applied query contract (docs/patterns/filter-bar.md §9); read by scripts/verify-quality-reference.mjs.
|
||||
export const DraftApplied: Story = { tags: ["verification"], name: "Draft / Applied", render: () => <Full showStatus /> }
|
||||
@@ -367,7 +367,8 @@ const sidebarGnbItems: NavItem[] = navItems.map(item => item.id === "projects" ?
|
||||
],
|
||||
} : item)
|
||||
|
||||
function SidebarPage({ fullHeight = false, color = "green" }: { fullHeight?: boolean; color?: "green" | "brown" }) {
|
||||
// `showCurrent={false}` keeps the representative page UI-only; the verification Story keeps the raw route readout.
|
||||
function SidebarPage({ fullHeight = false, color = "green", showCurrent = true }: { fullHeight?: boolean; color?: "green" | "brown"; showCurrent?: boolean }) {
|
||||
const [current, setCurrent] = useState("projects/design/tokens")
|
||||
const [groups, setGroups] = useState<Record<string, boolean>>({ projects: true, "projects/design": true })
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
@@ -412,7 +413,7 @@ function SidebarPage({ fullHeight = false, color = "green" }: { fullHeight?: boo
|
||||
<main data-hm-gnb-main>
|
||||
<div style={{ padding: contentStyle.padding }}>
|
||||
<h2 className="text-lg font-bold">프로젝트 업무 화면</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground" data-current-menu>선택: {current}</p>
|
||||
{showCurrent ? <p className="mt-2 text-sm text-muted-foreground" data-current-menu>선택: {current}</p> : null}
|
||||
<p className="mt-4 text-sm">선택한 메뉴의 콘텐츠를 표시합니다.</p>
|
||||
</div>
|
||||
</main>
|
||||
@@ -428,67 +429,47 @@ type Story = StoryObj<typeof meta>
|
||||
// Representative Story: the green bar with a current item, an open 2-depth panel, long labels, a logo, the optional utility icons and
|
||||
// sticky. Menus and the user menu open for real. Narrow the page to 390px for the mobile bar (hamburger → Sheet).
|
||||
// Each GNB fills its specimen parent inside the same Inner as the page and section headings.
|
||||
// The representative page names the connected area only; the verification Stories keep the default 12px join note.
|
||||
const contentLabel = "콘텐츠 영역"
|
||||
export const All: Story = {
|
||||
name: "GNB",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="GNB"
|
||||
description="1024px 이상 Desktop 메뉴 · 1023px 이하 햄버거 → Sheet"
|
||||
/>
|
||||
<StorySection
|
||||
title="Overview"
|
||||
description="현재 위치 Beige · hover Black 10% · 하위 패널은 hover / click"
|
||||
>
|
||||
<Page>
|
||||
<StoryPageHeader title="GNB" />
|
||||
<StorySection title="Overview">
|
||||
<Page label={contentLabel}>
|
||||
<Bar />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="States · Navigation Menu Open"
|
||||
description="하위 현재 위치 Beige · 긴 하위 이름은 패널 폭 확장"
|
||||
>
|
||||
<StorySection title="States · Navigation Menu Open">
|
||||
<div style={{ minHeight: 360 }}>
|
||||
<Page label="프로젝트 > 진행 중 프로젝트">
|
||||
<Bar initial="projects/active" openMenu="projects" />
|
||||
</Page>
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · Long Labels"
|
||||
description="브랜드·사용자는 말줄임 · 메뉴는 줄바꿈 없이 영역에서 잘림"
|
||||
>
|
||||
<Page>
|
||||
<StorySection title="Variants · Long Labels">
|
||||
<Page label={contentLabel}>
|
||||
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · With Logo"
|
||||
description="로고는 있을 때만 원본 비율 · 테스트 이미지(한맥 로고 아님)"
|
||||
>
|
||||
<Page>
|
||||
<StorySection title="Variants · With Logo">
|
||||
<Page label={contentLabel}>
|
||||
<Bar logo={testLogo} />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · Utilities"
|
||||
description="선택 사항 · 검색·알림은 GNBUtilities에 ghost Button 아이콘 조합"
|
||||
>
|
||||
<Page>
|
||||
<StorySection title="Variants · Utilities">
|
||||
<Page label={contentLabel}>
|
||||
<Bar utilities />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Usage · Sticky"
|
||||
description="스크롤 영역 상단에 고정"
|
||||
>
|
||||
<StorySection title="Usage · Sticky">
|
||||
<StickyPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · GNB Sidebar"
|
||||
description="기존 Reference 전용 · 신규 AI GNB_ONLY 화면에는 Sidebar 생성 금지">
|
||||
<SidebarPage />
|
||||
<StorySection title="Usage · GNB Sidebar">
|
||||
<SidebarPage showCurrent={false} />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB" description="Brown 선택·Hover · Green 선택색 혼용 금지"><SidebarPage color="brown" /></StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB"><SidebarPage color="brown" showCurrent={false} /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -469,9 +469,6 @@ function ResponsiveSpecimen({
|
||||
{/* Content paints above the navigation join strip, within the same LNB Mode. */}
|
||||
<div className="relative min-w-0 flex-1 max-lg:rounded-t-[var(--hm-lnb-mobile-join)] bg-[var(--hm-sem-bg-content)] p-[34px] lg:rounded-l-[var(--hm-lnb-join)]" data-story-slot="connected-content">
|
||||
<h2 className="text-lg font-bold">연결된 콘텐츠</h2>
|
||||
<p className="text-sm text-muted-foreground" data-current-menu>
|
||||
선택: {current ?? "없음"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -586,9 +583,6 @@ function Standalone({ mobileOpen = false, profileOpen = false }: { mobileOpen?:
|
||||
</div>
|
||||
<main className="relative min-h-0 min-w-0 flex-1 overflow-auto max-lg:rounded-t-[var(--hm-lnb-mobile-join)] bg-[var(--hm-sem-bg-content)] p-[34px] lg:rounded-l-[var(--hm-lnb-join)]" data-story-slot="connected-content">
|
||||
<h2 className="text-lg font-bold">연결된 콘텐츠</h2>
|
||||
<p className="text-sm text-muted-foreground" data-current-menu>
|
||||
선택: {current}
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -633,29 +627,26 @@ export const All: Story = {
|
||||
name: "LNB",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="LNB"
|
||||
description="1024px 이상 표시 · 1023px 이하 모바일 메뉴로 대체"
|
||||
/>
|
||||
<StorySection title="Overview" description="모든 뎁스 18px · 선택 Beige 면 + 초록 글자">
|
||||
<StoryPageHeader title="LNB" />
|
||||
<StorySection title="Overview">
|
||||
<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뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="States" description="선택 경로의 상위는 접혀도 노란색">
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ 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: "default", label: "Default", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: false }} /> },
|
||||
{ id: "expanded", label: "Expanded", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||
{ id: "selected-child", label: "Selected Child", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: true }} /> },
|
||||
{ id: "collapsed-with-selection", label: "Collapsed With Selection", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: false }} /> },
|
||||
{ id: "third-second-collapsed", label: "3뎁스 · 2뎁스 접힘", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": false }} /> },
|
||||
{ id: "third-first-collapsed", label: "3뎁스 · 1뎁스 접힘", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: false, "projects/design": true }} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -663,24 +654,21 @@ export const All: Story = {
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ 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 /> },
|
||||
{ id: "with-logo", label: "With Logo", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} /> },
|
||||
{ id: "long", label: "Long Labels", children: <ResponsiveSpecimen nodes={simpleNav(true)} initial="tasks/list" initialOpen={{ tasks: true }} longLabels /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Scroll" description="메뉴 영역만 세로 스크롤 · 브랜드·프로필 고정">
|
||||
<StorySection title="Usage · Scroll">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "scroll", label: "Scroll", children: <ResponsiveSpecimen nodes={simpleNav(false, true)} initial="overview" initialOpen={{ tasks: true }} height={460} /> },
|
||||
{ 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} /> },
|
||||
{ id: "third-scroll", label: "3뎁스 · Scroll", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Usage · LNB Standalone 반응형 (GNB 없음)"
|
||||
description="1023px 이하는 모바일 바 + Sheet"
|
||||
>
|
||||
<StorySection title="Usage · Standalone 반응형">
|
||||
<div className="flex flex-wrap items-start gap-6">
|
||||
<ViewportPreview story="standalone-desktop" label="Desktop" width={1024} height={640} />
|
||||
<ViewportPreview story="standalone-mobile" label="Mobile" width={390} height={640} />
|
||||
|
||||
@@ -32,10 +32,7 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Loading Screen"
|
||||
description="Inline은 부모 영역, fullscreen은 viewport 전체"
|
||||
/>
|
||||
<StoryPageHeader title="Loading Screen" />
|
||||
<StorySection title="Overview">
|
||||
<LoadingScreen />
|
||||
</StorySection>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
PageHeaderContent,
|
||||
PageHeaderDescription,
|
||||
PageHeaderTitle,
|
||||
Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
@@ -31,14 +31,11 @@ export const All: Story = {
|
||||
name: "Page Header",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
title="Page Header"
|
||||
description="기본형은 제목 중심 · 설명·액션·Breadcrumb는 선택"
|
||||
/>
|
||||
<StoryPageHeader title="Page Header" />
|
||||
<StorySection title="Overview">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>업무 현황</PageHeaderTitle></PageHeaderContent></PageHeader>
|
||||
</StorySection>
|
||||
<StorySection title="Breadcrumb 포함형" description="제목→설명 8px · Header→Tabs 20px">
|
||||
<StorySection title="Breadcrumb 포함형">
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
@@ -71,8 +68,7 @@ export const All: Story = {
|
||||
<TabsTrigger value="completed">완료 업무</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="mine" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>내 업무 일정 예시</TableCaption>
|
||||
<Table aria-label="내 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">마감일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<TableRow><TableCell>화면 설계서 검토</TableCell><TableCell>10월 14일</TableCell><TableCell>검토</TableCell></TableRow>
|
||||
@@ -81,8 +77,7 @@ export const All: Story = {
|
||||
</Table>
|
||||
</TabsContent>
|
||||
<TabsContent value="completed" className="px-0 py-6">
|
||||
<Table>
|
||||
<TableCaption>완료 업무 예시</TableCaption>
|
||||
<Table aria-label="완료 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">완료일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>프로젝트 기본 정보 등록</TableCell><TableCell>10월 9일</TableCell><TableCell>완료</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
|
||||
Reference in new issue
Block a user