9.4 KiB
Toggle Group
현재 HM 구현의 기준 문서. 공통 원칙은 docs/guidelines/프로젝트-지침.md(§6, §7), Token 구조 원칙은 테마-토큰-가이드.md를 따른다. 이 문서는 Toggle Group 고유의 API·Token·상태·결정만 기록한다. Toggle(toggle.md)과 역할 비교는 거기 §14.
1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-02 |
| 기반 | shadcn/ui Toggle Group (Radix ToggleGroup) |
| 대표 Story | Components/Form/Toggle Group → All (이름 "Toggle Group") |
| 보류 | 아래 §11 |
2. 적용 근거
- 참고 근거: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Toggle Group이 없다. 참고할 수치가 없었다.
- 현재 HM 규격: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Toggle과 같은 선택 언어(On = accent 면)를 공유하지만 Token은
--hm-toggle-group-*로 따로 두고 Toggle의 CSS에 의존하지 않는다. - 구현 방식: shadcn / Radix ToggleGroup 구조(Root + Item)와
variant·size·spacingAPI(context로 Item에 전달)를 유지한다. Item은 Toggle 컴포넌트가 아니라 독립 요소다. - Tabs와의 구분: Tabs는 content 전환(navigation), Toggle Group은 상태 선택이다.
- 보류: §11.
3. 기본 규격 (현재 HM 규격)
| 항목 | 값 |
|---|---|
| Item | 높이 sm 28 / default 34 / lg 42px, padding-x 8 / 12 / 16px, radius 4px, 14 / 20 / 500, icon + text gap 8px |
| Icon-only | 단일 React element + 직계 svg 하나인 경우 data-icon-only와 CSS로 padding 0, 높이와 같은 정사각형. SVG + text node는 size별 padding 8/12/16px 유지 |
| Off / On | Off는 투명 배경과 --hm-sem-fg-primary, On은 --accent 배경과 --accent-foreground |
outline variant |
1px --hm-sem-stroke-input-default border |
Connected (spacing={0}, 기본) |
안쪽 모서리 직각, 바깥 모서리 4px, outline border는 1px씩 겹침(-1px), On Item은 위로 올림(z-index) |
Separated (spacing > 0) |
각 Item이 독립 radius, 간격 = spacing × 4px |
| 방향 | horizontal(기본), vertical(orientation="vertical", 세로 연결 모서리) |
4. 상태 규격
| 상태 | 값 |
|---|---|
| Off / On | Radix data-state. single은 Item role="radio" + aria-checked(Root role="radiogroup"), multiple은 aria-pressed(Root role="toolbar") |
| Hover (off) | 배경 --accent 45% |
| Selected + Hover (on) | 배경 --accent 100%, 글자·아이콘 --accent-foreground 유지 |
Focus (:focus-visible) |
shadcn ring, focus된 Item이 인접 Item 위로(z-10) |
| Disabled (그룹 또는 Item) | opacity: 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), 클릭·focus 불가, 방향키 이동에서 제외 |
5. API
Radix ToggleGroup props 그대로. export: ToggleGroup, ToggleGroupItem.
ToggleGroup:type="single" | "multiple",value/defaultValue/onValueChange,orientation,disabled,rovingFocus,loop,dir. shadcn:variant,size,spacing(기본 0). 출력data-variant,data-size,data-spacing,data-orientation(orientation을 줄 때만).ToggleGroupItem:value(필수),disabled.variant/size는 그룹에서 내려온다.- single에서 선택된 Item을 다시 누르면 선택이 해제되어
onValueChange("")가 호출된다(Radix 동작). 항상 하나를 유지하려면 소비자가 빈 값을 무시한다(Story 참고). - icon-only Item은
aria-label을 준다. - 새 selection state 관리는 만들지 않았다.
6. Token
Component Mapping 위치: packages/ui/src/theme/toggle-group.css (--hm-toggle-group-*).
height-sm|md|lg, padding-x-sm|md|lg, radius, gap, font-size|line-height|font-weight, off-background|off-text, hover-background, on-background|on-text, border-width|color, disabled-opacity. Semantic과 Theme palette(--hm-sem-stroke-input-default)만 참조한다. --hm-toggle-*, --hm-button-*는 참조하지 않는다. 신규 Primitive와 Semantic 없음. --hm-toggle-group-gap은 spacing 값으로 인라인 style이 덮어쓴다.
7. shadcn / Radix에서 유지한 항목
Radix 구조·ARIA·roving focus(방향키, Home/End, disabled 건너뜀, loop)·Enter/Space·controlled/uncontrolled, shadcn context 방식의 variant/size/spacing, focus ring class, data-slot, cn(..., className).
8. HM에서 변경한 항목
- shadcn의
data-[spacing=0]:*계열 class와 Toggle 색 class를 제거하고 theme CSS로 이전. 연결형 모서리·border 겹침을 CSS 선택자로 처리. - Radix가
orientation을 주지 않으면data-orientation을 출력하지 않으므로 가로 연결형은:not([data-orientation="vertical"])로 선택한다. - 기본 모양을
@layer components에 두어 소비자의className이 이긴다.
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)을 한 페이지에 둔다. - verification:
Single,Multiple,Vertical. - meta에
component를 지정하지 않는다(props가single | multipleunion이라 args 없는 Story가 타입을 통과하지 못함).
10. 사용자 확정 결정
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Toggle Group은 Button과 독립 Component Token을 쓰며 Tabs와 같은 모양으로 만들지 않는다(작업 지시).
- 2026-10-06 Selected 원복: Git baseline
d101530의 On 매핑--accent/--accent-foreground를 복원했다. Selected + Hover도 Accent 100%와 동일 전경을 유지하고 아이콘은currentColor를 따른다. Border는 기존 default 투명 / outline--hm-sem-stroke-input-default를 유지한다. Active Semantic은 참조하지 않고 Focus는--ring→--hm-sem-stroke-input-focus(Light 2),ring-ring/50을 유지한다. Hover·Disabled·Icon-only / Icon+Text padding·Theme JSON·Runtime 계약 변경 없음.
11. 미확정 / 보류
- 수치(28/34/42px, 연결형 기본, On 면 accent, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
- 줄바꿈: 연결형(
spacing={0}) 그룹이flex-wrap으로 여러 줄이 되면 두 번째 줄 첫 Item의 border 겹침(-1px)과 모서리가 어색하다. 항목이 많거나 폭이 좁으면spacing > 0을 쓰고className="flex-wrap"을 소비자가 선택한다. 자동 responsive 배치는 만들지 않았다. - vertical 그룹의 폭: Item 폭이 서로 다르면 가장 넓은 Item에 맞춰 늘어난다(
items-stretch). - 선택 해제 가능 정책(single에서 재클릭)은 Radix 기본.
- 스크린리더 안내 문구(그룹 이름)는 소비자의
aria-label.
12. 관련 파일
packages/ui/src/components/ui/toggle-group.tsx
packages/ui/src/theme/toggle-group.css
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export)
src/components/toggle-group.stories.tsx
13. 검증 상태
-
2026-10-06 Selected 원복 검증: Storybook
All/ContentNodes에서 Default·실제 Hover·Selected·Selected + Hover·키보드 Focus·Disabled·Icon-only·Icon+Text 확인. Selected 배경rgb(189, 223, 212)/ 전경·아이콘rgb(18, 51, 40), outline borderrgb(222, 219, 210), Focus는 Light 2#26665150%의 3px ring. Runtime API의 Active override는 Selected에 영향 없음, Accent/on Accent override는 면·전경만 변경, Focus override는 ring만 변경. 1280/390px에서 sm/default/lg Icon-only 28/34/42px square·padding 0, Icon+Text/Text-only padding-x 8/12/16px 및 Array/Fragment 조합 유지.typecheck,build,build-storybook,git diff --check통과(기존 CSS 예시·Storybook chunk 경고 유지). -
2026-10-06 Visual QA: Icon+Text item도 기존
:has(> svg:only-child)에 실제 매칭됨을 확인하고 Toggle과 함께 수정. 1280/1024/768/390px, sm/default/lg에서 Icon-only PL/PR 0/0px·28/34/42px square, Icon+Text와 Text-only PL/PR 각각 8/12/16px, pressed 상태 정상. Array/Fragment 조합 확인.typecheck,build,build-storybook통과. -
npm run typecheck,npm run build: 통과(2026-10-02). -
실제 DOM(Browser pane): single은
radiogroup/radio(aria-checked), multiple은toolbar와aria-pressed. -
single keyboard: 포커스 후 ArrowRight로 다음 Item, Enter로 선택(
value = center), End로 마지막, Space로 선택(value = right), Home으로 처음. multiple: Enter/Space로 각각 토글되어[bold, italic, underline]. vertical: ArrowDown으로 이동, ArrowRight는 이동하지 않음. disabled: ArrowRight가 비활성 Item을 건너뛰어 A → C, 비활성 그룹은 클릭해도 상태 불변. (ArrowLeft는 첫 입력이 한 번 무시되는 경우가 있었고 두 번째 입력에서 A로 이동. 입력 도구의 타이밍으로 보이며 ArrowRight 건너뜀은 확인됨.) -
computed style: 연결형에서 첫/끝 Item만 바깥 모서리 4px, 가운데 0, outline Item margin
-1px, vertical은 위/아래 모서리,spacing={2}는 각 Item 4px radius. -
Responsive(390×844): 모든 그룹이 부모 폭 안에서 페이지 가로 overflow 없음,
spacing={1}+flex-wrap그룹은 358px 안에서 두 줄로 줄바꿈. -
미확인: 실제 pointer hover 면 시각, focus ring 시각,
loop={false}·rovingFocus={false}, RTL.