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.
8.6 KiB
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), Sliderole="group"+aria-roledescription="slide", Control은 disabled일 때 nativedisabled, 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, Sliderole=group/slide, 5개 slide, 초기 Previousdisabled, Control 32px·radius 9999px·흰 면,setApireadout이slide 1 / 5. - navigation: Next 클릭으로
slide 2 / 5와 Previous 활성화, Previous 클릭으로slide 1 / 5와 Previous 비활성화. 버튼에 focus를 둔 채 ArrowRight/ArrowLeft로 3 → 2. 끝(5/5)에서 Nextdisabled. - pointer: 왼쪽 drag로
slide 2 → 3, 오른쪽 drag로3 → 2. - loop: 첫 slide에서 Previous가 활성이고 클릭하면
3 / 3, Next로1 / 3, 계속 눌러도 Next가 비활성되지 않음. - vertical:
data-orientation=vertical, Previoustop: 8px·rotate: 90deg, Nextbottom: 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 동작.