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

8.6 KiB
Raw Permalink Blame History

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 동작.