Files
shkim4 4c29894e56 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.
2026-10-23 00:28:05 +09:00

117 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Carousel
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Carousel 고유의 API·Token·상태·결정만 기록한다.
## 1. 상태와 성격
| 항목 | 내용 |
|---|---|
| 성격 | **Interactive layout component** (slide 탐색). 단순 가로 scroll이 아니다 |
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-03 |
| 기반 | shadcn/ui Carousel = **Embla** (`embla-carousel-react` 8.6.0) |
| 대표 Story | `Components/Data & Display/Carousel` → `All` (이름 "Carousel") |
| 보류 | 아래 §12 |
## 2. 적용 근거
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Carousel이 없다. 참고할 수치가 없었다.
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Button/Scroll Area Token은 참조하지 않는다.
- **구현 방식**: Embla가 drag/swipe, snap, loop, align, plugin, scroll 계산을 소유한다. Carousel은 이전/다음 control, 방향키, carousel 접근성 속성을 더하고 Embla API를 `setApi`로 노출한다. 데이터 fetching이나 paging은 소유하지 않는다.
- **보류**: §12.
## 3. 기본 규격 (현재 HM 규격)
| 항목 | 값 |
|---|---|
| Slide 폭 | 기본 `basis-full`(한 개). 여러 개는 소비자가 `CarouselItem`에 `basis-1/2` 등을 준다. 카드 폭을 컴포넌트가 강제하지 않는다 |
| Gap | 16px (Track이 `-16px`만큼 당겨지고 각 Slide가 16px padding, 가로는 left / 세로는 top) |
| Control | 32 × 32px 원형 버튼, 흰 면 + 1px `--hm-sem-stroke-input-default` + `--hm-sem-fg-primary` 화살표 16px |
| Control 위치 | **carousel 가장자리 안쪽 8px**(가로: 좌우 가운데, 세로: 위/아래 가운데이며 화살표를 90° 회전). shadcn은 바깥(-48px)이라 좁은 부모에서 잘리는 문제가 있어 안쪽으로 정했다 |
| Content 커서 | `grab` / 누르는 동안 `grabbing` |
## 4. 상태 규격
| 상태 | 값 |
|---|---|
| Initial | Previous disabled(loop가 아니면), Next enabled |
| Disabled (경계) | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed. loop일 때는 비활성되지 않는다 |
| Hover | Control 배경 `--accent` |
| Focus (`:focus-visible`) | Control에 ring 모양 `0 0 0 3px ring 50%` |
| Dragging | Embla pointer drag, 커서 `grabbing` |
| Active slide | 컴포넌트 속성은 없다. `setApi`로 `selectedScrollSnap()`을 읽어 소비자가 표시한다(Story의 `slide n / total`) |
| Vertical | `orientation="vertical"`, Control이 위/아래로 이동하고 화살표가 회전 |
## 5. API
shadcn 구조 그대로. export: `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselPrevious`, `CarouselNext`, `useCarousel`, type `CarouselApi`.
- `Carousel`: `orientation` `"horizontal" | "vertical"`, `opts`(Embla options: `loop`, `align`, `dragFree`, `startIndex`…), `plugins`, `setApi`. 출력 `data-orientation`.
- `CarouselContent`: Embla viewport(`overflow-hidden`)와 track을 렌더한다(`data-slot="carousel-content"`, 안쪽 `carousel-track`). `className`은 track에 적용된다.
- `CarouselItem`: slide. `className`으로 `basis-*` 지정.
- `CarouselPrevious`/`CarouselNext`: Carousel 전용 `button`(Button 컴포넌트가 아님). sr-only 텍스트 "Previous slide" / "Next slide".
- `variant`, `size`, `cardType` 등은 만들지 않았다.
- **접근성**: Root `role="region"` + `aria-roledescription="carousel"`(이름은 소비자의 `aria-label`), Slide `role="group"` + `aria-roledescription="slide"`, Control은 disabled일 때 native `disabled`, ArrowLeft/ArrowRight가 이전/다음(캡처 단계에서 처리).
- **Root `min-w-0`**: grid/flex 부모의 `min-width:auto`가 Embla track의 intrinsic width로 커져 페이지가 밀리는 것을 막기 위해 Root에 `min-w-0`을 둔다. 한 단계 더 감싼 wrapper(grid item)에는 소비자가 `min-w-0`을 줘야 한다(Story가 그렇게 한다).
## 6. Dependency
- `embla-carousel-react` ^8.6.0 (`packages/ui`에 추가, shadcn Carousel의 기반). 사용: Carousel. CENTER 패키지에는 추가하지 않았다.
## 7. Token
Component Mapping 위치: `packages/ui/src/theme/carousel.css` (`--hm-carousel-*`).
`gap`, `control-size`, `control-inset`, `control-icon-size`, `control-background`, `control-text`, `control-border-width|color`, `control-hover-background`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`)만 참조한다. 신규 Primitive와 Semantic 없음. 모든 규칙이 `@layer components`에 있어 소비자의 utility(`className`: gap, control 위치와 크기)가 이긴다.
## 8. Embla에서 유지한 항목
pointer drag/swipe와 snap, loop·align·dragFree·skipSnaps 등 options, plugins, `canScrollPrev/Next`, `reInit`/`select` 이벤트, `scrollPrev/Next` 구현.
## 9. Storybook
- 대표 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. 소비자 확장성
- Slide 폭: `className="basis-1/2"`이 그대로 적용된다(확인).
- Gap/Control 위치·크기: CSS가 `@layer components`라 `className`의 utility가 이긴다(예: `CarouselPrevious className="-left-12"`로 바깥 배치 가능).
- Track 높이(세로): `CarouselContent style/className`으로 높이를 준다.
## 11. 사용자 확정 결정
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Control은 Button 조합이 아니라 Carousel 전용 control로 둔다(Button Token과 분리).
## 12. 미확정 / 보류
- 수치(32px control, 8px inset, 16px gap)는 현재 HM 규격이며 디자인 확정 시 재검토.
- Control이 slide 위에 겹치므로 카드 안쪽 콘텐츠를 가릴 수 있다(특히 세로 carousel의 첫/끝 항목). 바깥 배치는 `className`으로 선택한다.
- 인디케이터(dot), autoplay, 썸네일 동기화는 만들지 않았다(Embla plugin과 `setApi`로 소비자가 조합).
- 반응형(breakpoint별 `basis`)은 소비자 `className`(`md:basis-1/3`)이다. 자동 responsive carousel은 만들지 않았다.
- 포인터가 컨트롤 위일 때 drag 시작 동작, 스크린리더가 slide 변경을 알리는 live region은 정의하지 않았다.
## 13. 관련 파일
```
packages/ui/src/components/ui/carousel.tsx
packages/ui/src/theme/carousel.css
packages/ui/package.json (dependency: embla-carousel-react)
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export)
src/components/carousel.stories.tsx
```
## 14. 검증 상태
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
- 실제 DOM(Browser pane): Root `role=region`, `aria-roledescription=carousel`, `aria-label`, Slide `role=group`/`slide`, 5개 slide, 초기 Previous `disabled`, Control 32px·radius 9999px·흰 면, `setApi` readout이 `slide 1 / 5`.
- navigation: Next 클릭으로 `slide 2 / 5`와 Previous 활성화, Previous 클릭으로 `slide 1 / 5`와 Previous 비활성화. 버튼에 focus를 둔 채 ArrowRight/ArrowLeft로 3 → 2. 끝(5/5)에서 Next `disabled`.
- pointer: 왼쪽 drag로 `slide 2 → 3`, 오른쪽 drag로 `3 → 2`.
- loop: 첫 slide에서 Previous가 활성이고 클릭하면 `3 / 3`, Next로 `1 / 3`, 계속 눌러도 Next가 비활성되지 않음.
- vertical: `data-orientation=vertical`, Previous `top: 8px`·`rotate: 90deg`, Next `bottom: 8px`, Next 클릭 후 Previous 활성화, slide가 위아래로 쌓임.
- 레이아웃: Slide 폭이 Root 폭(`basis-full`), `basis-1/2`이면 절반(390px에서 187px), gap 16px(Track `-16px`, Item padding 16px).
- Responsive(390×844): 모든 carousel이 358px 안(좌 16px)에 들어가고 Control이 안쪽에 있어 잘리지 않으며 페이지 가로 overflow 없음. (처음에는 grid wrapper의 `min-width:auto` 때문에 페이지가 436px로 밀려 Root에 `min-w-0`과 Story wrapper의 `minWidth:0`을 추가해 해결했다.)
- 참고: 이 검증 환경은 페이지가 화면에 그려질 때만 animation frame이 진행되어, scroll 위치(translate)는 screenshot으로 frame을 만든 뒤에야 최종값에 도달했다. 위 검증은 Embla의 `selectedScrollSnap`(readout)과 버튼 상태 기준이다.
- 미확인: 터치 기기에서의 swipe(마우스 drag로 대체), `dragFree`·plugin(autoplay), 스크린리더 실제 읽기, 소비자의 `md:basis-*` breakpoint 동작.