140 lines
9.2 KiB
Markdown
140 lines
9.2 KiB
Markdown
# Checkbox
|
||
|
||
> 2026-10-10 추가 수정 요청 기준: `asChild`는 단일 React DOM 호환 요소를 받으며 소비자 자식·ID·이벤트·ref를 보존하고 내부 Indicator를 추가한다. 커스텀 요소는 props/ref를 최종 DOM으로 전달해야 한다. 기존 기본 button·외부 form 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
|
||
|
||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Checkbox 고유의 API·Token·상태·결정만 기록한다.
|
||
|
||
## 1. 상태
|
||
|
||
| 항목 | 내용 |
|
||
|---|---|
|
||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||
| 기준일 | 2026-10-02 |
|
||
| 기반 | shadcn/ui Checkbox (Radix `Checkbox`) |
|
||
| 대표 Story | `Components/Form/Checkbox` → `All` (이름 "Checkbox") |
|
||
| 보류 | 아래 §11 |
|
||
|
||
## 2. 적용 근거
|
||
|
||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Checkbox 문서가 없다(border·radius·indicator는 TBD, mark는 native UA 소유, HOLD). 구현 값으로 쓸 수 있는 확정 규격이 없어 참고만 했다.
|
||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(surface, stroke, action-primary, state-active, on-state-active)로 새로 정했다.
|
||
- **구현 방식**: shadcn / Radix Checkbox 구조(Root/Indicator, `data-state`, indeterminate, 키보드, Label 연결, 폼 동작)를 유지하고 theme CSS로 HM 스타일을 연결.
|
||
- **보류**: §11.
|
||
|
||
## 3. 기본 규격 (현재 HM 규격)
|
||
|
||
| 항목 | 값 | 비고 |
|
||
|---|---|---|
|
||
| Box | 18 × 18px | 현재 HM |
|
||
| Border | 1px `--hm-sem-stroke-default` | Semantic |
|
||
| Radius | `--hm-radius-control-md` (4px) | 공통 control radius |
|
||
| Face | `--hm-sem-bg-surface` | Semantic |
|
||
| Indicator | shadcn `CheckIcon` / `MinusIcon`, 14px, `--hm-sem-on-state-active` | shadcn 아이콘 유지 |
|
||
| Hit area | 18px box + 연결된 `Label` 클릭 | 별도 확장 없음 |
|
||
|
||
shadcn(lucide) 아이콘을 유지했다. 별도 HM glyph는 만들지 않았다.
|
||
|
||
## 4. 상태 규격
|
||
|
||
| 상태 | 값 |
|
||
|---|---|
|
||
| Unchecked | 위 기본 규격 |
|
||
| Checked (`data-state="checked"`) | border·face `--hm-sem-state-active`, 체크 아이콘 |
|
||
| Indeterminate (`data-state="indeterminate"`) | Checked와 같은 면, 마이너스 아이콘 |
|
||
| Hover (미체크) | border `--hm-sem-action-primary` |
|
||
| Hover (체크·indeterminate) | Black 20% overlay (`--hm-color-action-overlay-hover`) |
|
||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||
| Error (`aria-invalid="true"`) | border `--hm-sem-action-destructive` |
|
||
| Disabled | `opacity: 0.6` (이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed` |
|
||
|
||
## 5. API
|
||
|
||
Radix Checkbox props 그대로 (`checked` / `defaultChecked` / `onCheckedChange`, `"indeterminate"`, `disabled`, `required`, `name`, `value`, `form`). 추가 prop 없음. 출력 속성: `data-slot="checkbox"`, Radix `data-state`·`data-disabled`.
|
||
|
||
- Label과의 연결은 `<Label htmlFor>` 또는 감싸기. Label·Helper·Error 조합은 소비 조합(`field.md`).
|
||
|
||
### Form 연결 및 Reset
|
||
|
||
- Form 내부에서는 기존 native Form 연결을 사용한다. 외부에서는 `form="target"`과 `<form id="target">`으로 연결한다.
|
||
- 기본 버튼 렌더링에 `form`을 지정하면 HM은 Radix의 공개 `asChild` 경로로 같은 Checkbox 버튼에도 `form`을 전달한다. 추가 DOM wrapper, 별도 상태, Reset listener 또는 숨김 input 강제 조작은 없다. 호출자가 명시한 `asChild` 경로는 기존대로 유지한다.
|
||
- Uncontrolled는 `defaultChecked`로 초기 상태를 지정한다. Native Reset 버튼과 `form.reset()`은 Radix가 보관한 최초 상태를 복원한다. 이후 `defaultChecked` 변경을 현재 선택값 변경으로 사용하지 않는다.
|
||
- Controlled는 부모의 `checked`가 상태의 기준이다. 부모 `onReset`에서 초기값을 복원하고 `onCheckedChange`로 변경 요청을 반영한다. 외부 Form에도 내부 Form과 같은 Radix Reset callback 계약이 적용된다.
|
||
- Checked는 `name` / `value`로 제출된다(기본 `value="on"`). Unchecked와 indeterminate는 제출되지 않는다. Indeterminate는 `aria-checked="mixed"`이며 native input의 `checked`는 false다. Disabled는 버튼과 숨김 input 모두 비활성화되어 제출되지 않는다.
|
||
|
||
```tsx
|
||
const initialChecked = true
|
||
const [checked, setChecked] = React.useState(initialChecked)
|
||
|
||
return (
|
||
<>
|
||
<form id="target" onReset={() => setChecked(initialChecked)}>
|
||
<button type="reset">초기화</button>
|
||
</form>
|
||
<Checkbox
|
||
form="target"
|
||
name="agreement"
|
||
checked={checked}
|
||
onCheckedChange={setChecked}
|
||
/>
|
||
</>
|
||
)
|
||
```
|
||
|
||
Uncontrolled 예제는 `checked` / `onCheckedChange` 대신 `defaultChecked={true}`를 사용하며 부모의 Reset 상태 관리는 필요하지 않다.
|
||
|
||
## 6. Token
|
||
|
||
Component Mapping 위치: `packages/ui/src/theme/checkbox.css` (`--hm-checkbox-*`).
|
||
|
||
`size`, `radius`, `border-width`, `icon-size`, `background`, `border-color`, `hover-border-color`, `checked-background`, `checked-border-color`, `checked-hover-overlay`, `indicator-color`, `error-border-color`. Semantic(`--hm-sem-*`)을 참조하고 Input 등 다른 컴포넌트 Token을 쓰지 않는다. Primitive 신규 추가 없음(`--hm-sem-action-destructive`는 기존 Primitive를 직접 참조).
|
||
|
||
## 7. shadcn / Radix에서 유지한 항목
|
||
|
||
Radix 구조·ARIA(`role=checkbox`, `aria-checked`)·키보드, `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none` + focus ring, disabled 커서, lucide `CheckIcon`.
|
||
|
||
## 8. HM에서 변경한 항목
|
||
|
||
- shadcn `size-4 rounded-[4px] border shadow-xs`, `dark:` 및 `data-[state=checked]:bg-primary` 계열 class 제거 → theme CSS.
|
||
- `MinusIcon` 추가(shadcn 기본은 indeterminate 아이콘이 없다). 두 아이콘은 `data-state`에 따라 CSS로 전환.
|
||
- Disabled opacity 50 → 60.
|
||
|
||
## 9. Storybook
|
||
|
||
- 대표 Story `All`("Checkbox"): `unchecked`, `checked`, `indeterminate`, `interactive`(controlled), `hover`, `focus`, `error`, `disabled`, `long`. Focus는 `play`로 실제 focus.
|
||
- verification: `Unchecked`, `Checked`, `Indeterminate`, `Disabled`, `Error`, `Playground`.
|
||
|
||
## 10. 사용자 확정 결정
|
||
|
||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||
|
||
## 11. 미확정 / 보류
|
||
|
||
- Checkbox Group 간격, group Label·Error 슬롯, option label 전경 role: 미결정.
|
||
- 수치(18px, 14px 아이콘 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||
- Checkbox Group, Error 문구 슬롯: 컴포넌트 없음(소비 조합).
|
||
- Hit area 확장(24px 타깃 등) 미적용.
|
||
- Disabled opacity `0.6`: 이 컴포넌트 값이며 전역 Disabled 규칙으로 승격하지 않는다.
|
||
|
||
## 12. 관련 파일
|
||
|
||
```
|
||
packages/ui/src/components/ui/checkbox.tsx
|
||
packages/ui/src/theme/checkbox.css
|
||
packages/ui/src/theme.css (import)
|
||
packages/ui/src/index.ts (export Checkbox)
|
||
src/components/checkbox.stories.tsx
|
||
```
|
||
|
||
## 13. 검증 상태
|
||
|
||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||
- Storybook computed style: 18×18, border 1px `rgb(154,146,132)`, radius 4px; Checked/Indeterminate bg·border `rgb(18,51,40)`; Error border `rgb(211,47,47)`; Disabled opacity 0.6; 체크/마이너스 아이콘이 상태에 따라 하나만 표시.
|
||
- 실제 interaction: controlled indeterminate → click → checked → click → unchecked, Label 클릭으로 토글 확인.
|
||
- 2026-10-08, React 19.3 / Radix Checkbox 1.3.11: 프로젝트 외부 격리 환경의 수정된 HM 패키지로 실제 브라우저 검증. 수정 전 외부 Form의 true·false·indeterminate Reset 불일치를 재현했고, 수정 후 모두 PASS.
|
||
- Form 내부·외부에서 Space로 상태 변경 후 Native Reset 버튼 및 `form.reset()`을 실행: `defaultChecked` true·false·indeterminate와 Controlled 부모 `onReset`의 상태·ARIA·숨김 input·FormData 복원 확인. Indeterminate 복원 시 숨김 input은 `checked=false`, `indeterminate=true`, 제출값 없음.
|
||
- 기존 Radix 동작도 구분해 확인: 최초 indeterminate 렌더에서는 숨김 input의 `indeterminate` 속성이 false다(내부·외부 및 수정 전·후 동일). UI/ARIA는 mixed, input의 `checked=false`, FormData 제외는 정상이며 이번 외부 Form Reset 수정에서 이 초기화 동작은 변경하지 않는다.
|
||
- Native Form HTTP POST 8건: 내부·외부·Controlled의 Checked 값은 `choice=accepted`, Unchecked·indeterminate·Disabled 값은 제외. `required` native 유효성 및 ARIA, Disabled Tab 제외, BUTTON ref 유지 확인.
|
||
- 수정 전·후 같은 Theme에서 Checked·Unchecked·Indeterminate·Disabled computed style과 아이콘 일치: 18×18px, border 1px, radius 4px, icon 14px, Disabled opacity 0.6. 키보드 focus-visible 3px ring도 일치. CSS·Theme·대표 Story 변경 없음.
|
||
- UI 전체 타입 검사 및 공식 UI 패키지 빌드를 격리 경로에서 통과. 테스트에 사용한 Checkbox 소스와 실제 수정본의 SHA-256 일치. 공용 dist는 이 검증에서 빌드하지 않음.
|
||
- 미확인: 실제 pointer hover, 사이드바 화면, 390px 폭. Hover 관련 CSS와 class는 변경하지 않음.
|