114 lines
9.7 KiB
Markdown
114 lines
9.7 KiB
Markdown
# Toggle Group
|
||
|
||
> 2026-10-10 추가 수정 요청 기준: 소비자 style과 내부 --hm-toggle-group-gap을 병합. spacing은 4px 단위이며 명시적 동일 CSS 변수 override는 기존 확장 계약으로 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
||
|
||
현재 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`·`spacing` API(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 | multiple` union이라 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 border `rgb(222, 219, 210)`, Focus는 Light 2 `#266651` 50%의 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.
|