Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
@@ -0,0 +1,112 @@
|
||||
# Accordion
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Accordion 고유의 API·Token·상태·결정만 기록한다. Collapsible(`collapsible.md`)과 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Accordion (Radix `Accordion`) |
|
||||
| 대표 Story | `Components/Disclosure/Accordion` → `All` (이름 "Accordion") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Accordion이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg, stroke, divider 계열 Primitive)로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Accordion 구조(Root·Item·Header·Trigger·Content), shadcn chevron과 open/close animation을 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **Collapsible과의 구분**: Accordion은 여러 item이 연속된 정보 구조라 item 사이 divider가 구조를 만든다. 단일 독립 disclosure는 Collapsible을 쓴다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 | 비고 |
|
||||
|---|---|---|
|
||||
| Container | 바깥 박스·배경 없음 | |
|
||||
| Item divider | 1px `--hm-sem-stroke-input-default`, 마지막 item 제외 | Dialog·Card와 같은 윤곽색(값 참조만) |
|
||||
| Trigger | min-height 48px, padding `12px 0`, gap 16px, 위 정렬이 아니라 세로 가운데 | |
|
||||
| Trigger typography | 16 / 24 / 500 | |
|
||||
| Trigger 전경 | `--hm-sem-fg-primary` | |
|
||||
| Indicator | shadcn `ChevronDownIcon` 20px, `--hm-sem-fg-secondary`, 열리면 180° 회전(200ms) | shadcn 유지 |
|
||||
| Content | 14 / 20 / 400, `--hm-sem-fg-primary`, 아래 padding 16px | 안쪽 div에 padding |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere`, 고정 높이 없음 | |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state` |
|
||||
| Hover | Trigger background `transparent`, cursor pointer. 기존 underline/focus 정책 유지, disabled 제외 |
|
||||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, 키보드 이동에서 제외 |
|
||||
| Open / Close animation | shadcn `accordion-down` / `accordion-up` 0.2s ease-out (height). 첫 렌더에서는 Radix가 animation을 막는다 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Accordion props 그대로. export: `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`.
|
||||
|
||||
- `Accordion`: `type="single" | "multiple"`, `collapsible`(single), `value`/`defaultValue`/`onValueChange`, `disabled`, `orientation`, `dir`.
|
||||
- `AccordionItem`: `value`(필수), `disabled`.
|
||||
- `AccordionTrigger`: chevron을 내장한다. 출력 `data-slot="accordion-trigger"`, 아이콘 `data-slot="accordion-trigger-icon"`.
|
||||
- `AccordionContent`: `className`은 안쪽 div(`data-slot="accordion-content-inner"`)에 적용된다. Radix Content(`data-slot="accordion-content"`)는 `overflow-hidden`과 animation만 가진다.
|
||||
- `variant`, `size`, `appearance` 등 HM 전용 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/accordion.css` (`--hm-accordion-*`).
|
||||
|
||||
`text`, `content-text`, `divider-width`, `divider-color`, `trigger-min-height`, `trigger-padding-y`, `trigger-gap`, `trigger-font-size|line-height|font-weight`, `icon-size`, `icon-color`, `content-font-size|line-height`, `content-padding-bottom`, `disabled-opacity`. Semantic과 Theme palette만 참조하고 다른 컴포넌트의 Component Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`aria-expanded`, `aria-controls`, `role=region`)·키보드(Arrow Up/Down은 trigger 사이 이동하며 끝에서 순환, Home/End, Enter/Space)·single/multiple/collapsible 동작, Header 요소(`h3`), `data-slot`, `cn(..., className)`, chevron 회전, `accordion-down/up` animation, focus ring.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `py-4 text-sm font-medium hover:underline`, `size-4` 아이콘, Item `border-b`, Content `pb-4 text-sm` class를 제거하고 theme CSS로 이전.
|
||||
- shadcn은 Content 안쪽 div가 없는 구조도 있었으나 height animation이 padding에 의해 흔들리지 않도록 padding은 안쪽 div에 둔다.
|
||||
- Trigger의 `disabled:pointer-events-none`을 `disabled:cursor-not-allowed`로 교체(커서가 보이도록).
|
||||
- animation을 `animate-[accordion-down_0.2s_ease-out]` 임의 값으로 연결(`animate-accordion-*` utility theme 변수가 이 패키지에 없다. keyframe은 `shadcn/tailwind.css`가 제공).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Accordion"): `StateBoard`로 Single(첫 항목 열림), Collapsible Single, Multiple(두 항목 열림), Disabled Item, Long Content(여러 줄 Trigger + 긴 콘텐츠 + 공백 없는 문자열)를 한 페이지에 둔다. 모두 실제 클릭·키보드로 동작.
|
||||
- verification: `Single`, `Multiple`, `Disabled`, `Long Content`.
|
||||
- meta에 `component`를 지정하지 않는다(Accordion props가 `single | multiple` union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(48px, 16/24/500, padding, divider 색, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 현재 Hover는 `transparent`다. Focus Light 2 50% 3px ring과 Disabled opacity 0.6·cursor not-allowed는 유지.
|
||||
- Indicator는 shadcn chevron 유지(Select의 HM glyph와 별개). 별도 Source glyph 없음.
|
||||
- Motion 규칙이 없어 shadcn animation 유지. `prefers-reduced-motion` 대응은 추가하지 않았다(기존 구현에도 없음).
|
||||
- Container 외곽 박스/Card형 variant는 만들지 않았다.
|
||||
- 소비 조합: Trigger 안 아이콘·부가 정보 슬롯은 정의하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/accordion.tsx
|
||||
packages/ui/src/theme/accordion.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/accordion.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
> HISTORICAL_RECORD: 아래 날짜별 검증은 당시 구현의 기록이다. 과거 soft surface 측정은 보존하며, 현재 Hover 규격은 §4의 `transparent`를 따른다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 Closed/Open Hover의 전체 soft surface·pointer, Focus 3px ring, Disabled opacity 0.6·not-allowed 및 가로 overflow 없음 확인. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): ArrowDown으로 trigger 이동(비활성 항목 건너뜀, 끝에서 처음으로 순환), Home/End, Enter로 열기, Space로 닫기, single(다른 항목을 열면 이전 항목 닫힘, 열린 항목 재클릭은 닫히지 않음), collapsible single(재클릭으로 모두 닫힘), multiple(여러 항목 동시 열림), disabled 항목 클릭 무반응(`disabled`, `data-disabled`).
|
||||
- animation: 열 때 `accordion-down`이 적용되고 높이가 5 → 36px로 변하는 것을 샘플링으로 확인.
|
||||
- computed style: trigger 48px·16/24/500, 전경 `rgb(10,34,29)`, divider `1px rgb(222,219,210)`, icon 20px `rgb(110,103,89)`, content 14/20·아래 padding 16px. 긴 콘텐츠에서 가로 overflow 없음, 긴 Trigger는 여러 줄로 줄바꿈.
|
||||
- 미확인: ArrowUp(ArrowDown과 대칭), 닫힘 animation 시각, 실제 pointer hover/focus ring 시각, 390px 폭.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Alert Dialog
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Alert Dialog 고유의 API·Token·상태·결정만 기록한다. Dialog(`dialog.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Alert Dialog (Radix `AlertDialog`) |
|
||||
| 대표 Story | `Components/Overlay/Alert Dialog` → `All` (이름 "Alert Dialog") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 Dialog 문서가 Alert Dialog를 FOLLOW_UP(DIALOG-03)으로만 두고 규격은 없다. 의도(확인·취소를 명시적으로 요구하는 중요한 결정)를 참고했다. 현재 Button 조합은 사용자 확정 규칙에 따라 일반 확인은 취소 Gray Solid / 확인 Green Solid, 삭제 확인은 취소 Gray Outline / 삭제 Red Outline이며 같은 세트의 appearance·size를 통일한다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog의 접근성 원칙과 `asChild` 처리 규칙은 재사용하고 수치는 현재 HM Semantic으로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix AlertDialog 구조 유지. Dialog + destructive 플래그로 합치지 않고 별도 컴포넌트로 둔다.
|
||||
- **Dialog와의 차이**: Close(X) 없음, 바깥 클릭으로 닫히지 않음, 초기 focus가 Cancel, Body·Header divider 없음, 폭 400px 고정 상한.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border / Radius / Shadow | 1px `--hm-sem-stroke-input-default` / 8px / `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| 위치 | viewport 가운데(shadcn 기본) |
|
||||
| 폭 | `min(400px, 100vw − 32px)`, 최대 높이 `100dvh − 32px`(넘치면 content scroll) |
|
||||
| Padding | 24px 사방 |
|
||||
| Title | 18 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary`, Title과 gap 8px |
|
||||
| Footer(Actions) | Header와 24px 간격, gap 8px, 오른쪽 정렬(좁은 화면은 세로, Cancel이 아래) |
|
||||
| Action / Cancel | HM Button(아래 §5) |
|
||||
| Overlay / animation | shadcn 기본(`bg-black/10` + blur, fade + zoom) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock, `role=alertdialog` |
|
||||
| 초기 focus | **Cancel**(Radix AlertDialog 기본: 파괴적 action 대신 안전한 쪽) |
|
||||
| 닫힘 조건 | Cancel, Action, Escape. **바깥 클릭으로는 닫히지 않음** |
|
||||
| focus 복귀 | 닫히면 trigger로 복귀 |
|
||||
| Confirmation / Destructive | 컴포넌트 상태가 아니라 Button 조합(§5) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix AlertDialog props 그대로. export: `AlertDialog`, `AlertDialogTrigger`, `AlertDialogPortal`, `AlertDialogOverlay`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogFooter`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel`.
|
||||
|
||||
- `AlertDialogAction`: HM Button을 Radix Action 위에 `asChild`로 합성(`<Button asChild><AlertDialogPrimitive.Action/></Button>`). 기본 `variant="primary"` `appearance="solid"`, `size` 기본 `default`. 위험한 작업은 `variant="destructive" appearance="outline"`.
|
||||
- `AlertDialogCancel`: 같은 방식, 기본 `variant="cancel"` `appearance="solid"`.
|
||||
- `AlertDialogAction variant="destructive"`만 지정하면 기본 `solid`가 적용되어 **미지원 Destructive Solid**가 된다. 삭제 확인은 Action과 Cancel 모두 `appearance="outline"`을 명시하고 같은 size를 사용한다. 자동 appearance 변경은 없다. [Button 허용 조합·기본값 주의](button.md#62-기본값별칭합성-주의)를 따른다.
|
||||
- 합성한 Radix 요소에는 `data-slot`을 붙이지 않는다. 붙이면 `asChild` Slot 병합에서 자식 값이 이겨 Button의 `data-slot="button"`이 사라지고 HM Button 스타일이 적용되지 않는다(Dialog에서 해결한 문제와 같은 원인). 출력 속성은 Button의 `data-slot="button"`, `data-variant`, `data-appearance`, `data-size`.
|
||||
- Alert Dialog 자체에는 Button 스타일을 정의하지 않는다. Button API(`variant`/`appearance`/`size`)만 전달한다.
|
||||
- `AlertDialogContent`에 Close(X)는 없다. 새 prop(`size`, `variant`, `destructive`)은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/alert-dialog.css` (`--hm-alert-dialog-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `width`, `edge`, `padding`, `header-gap`, `actions-gap`, `actions-margin-top`, `title-*`, `description-*`. Semantic과 Theme palette만 참조하며 `--hm-dialog-*`를 참조하지 않는다. 신규 Primitive 없음. Button 색은 Button 문서의 token을 따른다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix AlertDialog 구조 전부: Portal·Overlay·modal·focus trap·초기 focus(Cancel)·Escape·바깥 클릭 무시·focus 복귀·`role=alertdialog`·`aria-labelledby/describedby`, 중앙 위치 class, 열림/닫힘 animation class, `data-slot`(Content·Header·Footer·Title·Description).
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-background`, `ring-1`, `rounded-xl`, `p-4`, `gap-4`, `max-w-xs`/`sm:max-w-sm`, size 변형 제거 → theme CSS.
|
||||
- `AlertDialogAction`/`Cancel`을 `buttonVariants` class 적용 방식에서 **HM Button 합성**으로 변경. 선택지 `variant`/`appearance`/`size`를 Button API로 받는다.
|
||||
- 방향별 Header/Footer 배치와 `AlertDialogMedia` 같은 shadcn 부가 요소는 추가하지 않았다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Alert Dialog"): 트리거 2개(로그아웃 = 일반 확인, 삭제 = destructive)가 각각 실제 Alert Dialog를 연다.
|
||||
- verification: `Confirmation`, `Destructive`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(400px, 18px Title, 24px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay 색·blur, Motion은 shadcn 기본 유지.
|
||||
- 파괴적 작업의 기본 강조(Red outline)는 Dialog 삭제 확인과 같은 조합이며 Alert Dialog 고유 규칙이 아니다.
|
||||
- 아이콘/Media slot, non-dismissible 정책 변경(예: Escape 막기)은 만들지 않았다.
|
||||
- 소비 화면의 공통 header/footer 높이 대응 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/alert-dialog.tsx
|
||||
packages/ui/src/theme/alert-dialog.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/alert-dialog.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 키보드(Enter)로 열기, `role=alertdialog`, 초기 focus가 "취소", Tab으로 "삭제" 이동 후 다시 Tab하면 "취소"로 순환(focus trap), 바깥 클릭에 닫히지 않음, Escape 닫힘과 trigger focus 복귀, Cancel·Action 클릭 닫힘과 trigger 복귀, Close(X) 없음.
|
||||
- Button 합성: Action·Cancel 모두 `data-slot="button"`, `data-variant`(destructive/cancel), `data-appearance`가 유지되고 HM 색(Cancel `rgb(118,118,118)`, 삭제 글자 `rgb(211,47,47)`)이 적용됨.
|
||||
- computed style: width 400px, radius 8px, padding 24px, shadow `0 0 12px .2`.
|
||||
- 390×844: 폭 358px(좌우 16px), Footer 세로 쌓임(Cancel 아래), 가로 overflow 없음.
|
||||
- 미확인: Enter/Space로 Action 키보드 실행, Radix `AlertDialogAction` 비동기 처리, 닫기 animation 시각.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Alert
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Alert 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Alert (CVA, grid + 선행 svg) |
|
||||
| 대표 Story | `Components/Feedback/Alert` → `All` (이름 "Alert") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Alert 규격이 없다(후보 목록, 상태 색 success/warning/information 미정). Toast와 별개로 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: shadcn Alert 구조(`role="alert"`, 선행 svg 아이콘 grid, `AlertTitle`, `AlertDescription`)를 유지하고 `data-variant`로 theme CSS를 선택.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Container | border 1px, radius 4px, padding `12px 16px`, 면 `--hm-sem-bg-surface` |
|
||||
| Icon | 16px (소비자가 lucide 등 svg를 첫 자식으로), 컬럼 gap 12px |
|
||||
| Title | 14 / 20 / 500 |
|
||||
| Description | 14 / 20 / 400, 색 `--hm-sem-fg-secondary` |
|
||||
| Close action | 없음 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| variant | border | Title·icon 색 |
|
||||
|---|---|---|
|
||||
| `default` | `--hm-sem-stroke-default` | `--hm-sem-fg-primary` |
|
||||
| `destructive` | `--hm-sem-action-destructive` | `--hm-sem-action-destructive` |
|
||||
|
||||
아이콘이 없으면 텍스트가 왼쪽에서 시작한다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Alert: variant?: "default" | "destructive" 기본 "default"
|
||||
AlertTitle, AlertDescription
|
||||
```
|
||||
`Alert`는 `role="alert"`로 렌더. 출력 속성: `data-slot="alert|alert-title|alert-description"`, `data-variant`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/alert.css` (`--hm-alert-*`): `radius`, `padding-x/y`, `border-width`, `icon-size`, Title/Description typography, variant별 `{default,destructive}-{background,border-color,title-color,description-color}`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
`role="alert"`, grid 레이아웃(`has-[>svg]`), `data-slot`, `cn(..., className)`, `AlertTitle` line-clamp.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `rounded-lg border px-4 py-3 text-sm`, destructive의 색 class 제거 → theme CSS. 아이콘을 16px로 고정.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `default`, `destructive`, `no-icon`, `title-only`, `long`. verification: `Default`, `Destructive`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- success / warning / information variant: HM 상태 색 미정이라 만들지 않았다(neutral·destructive만).
|
||||
- 닫기 버튼, action 슬롯: 없음. Toast와 혼합하지 않는다.
|
||||
- 수치(12/16 padding, 16px 아이콘)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/alert.tsx
|
||||
packages/ui/src/theme/alert.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Alert, AlertTitle, AlertDescription)
|
||||
src/components/alert.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: `role=alert`, border 1px, radius 4px, padding `12px 16px`, Title 14px/500, destructive border·Title `rgb(211,47,47)`, 아이콘 16px, 아이콘 유무에 따른 Title 시작 위치(45px / 17px) 확인.
|
||||
- 미확인: 사이드바 화면, 390px 폭.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Aspect Ratio
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Aspect Ratio 고유의 API·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Layout primitive** (비율 유지). 시각 디자인이 없는 utility |
|
||||
| 구현 상태 | 구현 완료 |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Aspect Ratio = Radix `AspectRatio` |
|
||||
| 대표 Story | `Components/Data & Display/Aspect Ratio` → `All` (이름 "Aspect Ratio") |
|
||||
| 보류 | 아래 §10 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Aspect Ratio가 없다. 참고할 수치가 없다.
|
||||
- **현재 HM 규격**: 시각 규격이 없다. 비율 박스만 제공하며 surface, background, radius, border, 이미지 처리는 소비자가 자식에 구성한다(HM 디자인을 이유로 이미지 스타일을 내부에 넣지 않는다).
|
||||
- **구현 방식**: Radix AspectRatio를 그대로 감싼다(`data-slot="aspect-ratio"`).
|
||||
- **보류**: §10.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Ratio | `ratio` prop(너비 / 높이, 예: `16 / 9`). 기본 1 |
|
||||
| 폭 | 부모 폭 100%(Radix wrapper가 `padding-bottom`으로 높이를 계산) |
|
||||
| 시각 | 없음(배경, 윤곽, radius 모두 투명/0) |
|
||||
| 자식 | Radix가 `position:absolute; inset:0`으로 채운다. 이미지는 소비자가 `size-full object-cover rounded-md` 등으로 구성 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix AspectRatio props 그대로. export: `AspectRatio`.
|
||||
|
||||
- `ratio`, `asChild`, 그리고 `div` props. `size`, `variant`, `mediaType`은 만들지 않았다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
새 dependency 없음(`radix-ui`에 이미 포함).
|
||||
|
||||
## 7. Token / Theme CSS
|
||||
|
||||
**없다.** 시각 규격이 없어 `--hm-aspect-ratio-*` Token과 `aspect-ratio.css`를 만들지 않았고 `theme.css`에도 연결하지 않았다.
|
||||
|
||||
## 8. 소비자 확장성
|
||||
|
||||
- 폭은 부모가 정한다(Story는 wrapper의 `max-width`). `className`/`style`이 그대로 적용된다.
|
||||
- 이미지 fit, radius, border는 자식 요소에서 소비자가 정한다. Card와 조합해도 AspectRatio는 Card에 의존하지 않는다(Story에서 Card 안에 4:3으로 조합).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Aspect Ratio"): 16:9, 4:3, 1:1 이미지(inline data URI, 네트워크 비의존), video-like placeholder, Card 안 조합을 한 페이지에 둔다.
|
||||
- verification: `16:9`, `1:1`.
|
||||
|
||||
## 10. 사용자 확정 결정 / 보류
|
||||
|
||||
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 시각 규격은 정하지 않는다.
|
||||
- 보류: 비율 preset(예: `ratio="video"`), 이미지 로딩 placeholder, 모서리 radius 기본값은 만들지 않았다.
|
||||
- CSS `aspect-ratio` 속성 기반 구현(Radix는 `padding-bottom` hack)은 Radix 구현을 따르며 바꾸지 않았다.
|
||||
|
||||
## 11. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/aspect-ratio.tsx
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/aspect-ratio.stories.tsx
|
||||
```
|
||||
|
||||
## 12. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 레이아웃(Browser pane, 390×844): 16:9는 320×180(비율 1.778), 4:3은 320×240(1.333), 1:1은 320×320(1.000), video placeholder 320×180, Card 안 4:3은 270×203(1.333). wrapper style은 `position: relative; width: 100%; padding-bottom: 56.25%` 등 Radix 계산이며 이미지가 같은 크기로 채워지고(`object-fit: cover`, 로드됨) 배경·border·radius가 없다. 페이지 가로 overflow 없음.
|
||||
- 부모 폭이 바뀌면 높이가 비율대로 변한다(Story의 max-width 320 → 390px 뷰포트에서 320, Card 안에서는 270).
|
||||
- 미확인: 부모 폭을 연속으로 resize할 때의 reflow, `ratio` 동적 변경.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Avatar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Avatar 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Avatar (Radix `Avatar`) |
|
||||
| 대표 Story | `Components/Data & Display/Avatar` → `All` (이름 "Avatar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Avatar가 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Semantic(accent 면)으로 새로 정했다. size 체계는 Button/Input size를 복사하지 않고 shadcn Avatar API(`sm`/`default`/`lg`)에 맞춰 24/32/40px로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Avatar 구조(Root, Image, Fallback)를 유지하고 theme CSS로 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `sm` 24px, `default` 32px, `lg` 40px (정사각형) |
|
||||
| Shape | 원형(radius 9999px), `overflow: hidden` |
|
||||
| Image | `size-full`, `object-cover`, `aspect-square` (중앙 crop) |
|
||||
| Fallback 면 | `--accent` (green tint), 글자 `--accent-foreground` |
|
||||
| Fallback typography | weight 500, `sm` 10px / `default` 12px / `lg` 14px, line-height 1, 한 줄 `nowrap`, 넘치면 잘림 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| 이미지 로드 성공 | Radix가 Image를 렌더하고 Fallback은 렌더하지 않는다 |
|
||||
| 이미지 로드 실패 / 로딩 중 | Fallback 표시(`delayMs` prop으로 지연 가능) |
|
||||
| Focus / Disabled | 없음(비 interactive) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Avatar props 그대로. export: `Avatar`, `AvatarImage`, `AvatarFallback`.
|
||||
|
||||
- `Avatar`: `size` `"sm" | "default" | "lg"`(기본 `default`, shadcn API 유지). 출력 `data-size`.
|
||||
- `AvatarImage`: `src`, `alt`, `onLoadingStatusChange`. `AvatarFallback`: `delayMs`.
|
||||
- 접근성: 이름이 보이는 곳에서는 `alt=""`, 단독으로 쓰면 `alt`로 이름을 준다.
|
||||
- 새 prop은 만들지 않았다. 크기는 `size` prop(또는 `--hm-avatar-size-*` Token)으로만 바꾼다. theme CSS가 레이어 밖이라 `className="size-12"` 같은 utility는 이기지 못한다.
|
||||
- shadcn 최신 버전에 있는 `AvatarBadge`/`AvatarGroup`은 만들지 않았다(필요가 생기면 추가).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/avatar.css` (`--hm-avatar-*`).
|
||||
|
||||
`size-sm|md|lg`, `radius`, `fallback-background`, `fallback-text`, `fallback-font-weight`, `fallback-font-size-sm|md|lg`. Semantic(`--accent`, `--accent-foreground`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 이미지 로드 상태 처리와 Fallback 표시 규칙, 구조, `size` prop, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `size-8`/`data-[size=lg]:size-10`, `bg-muted` fallback, 글자 크기 class를 제거하고 Token으로 연결. 크기는 24/32/40.
|
||||
- Fallback 면을 `muted`가 아니라 accent(green tint)로 했다(HM 팔레트에서 대비가 있는 면).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Avatar"): Image(sm/default/lg, data URI 이미지로 네트워크 비의존), Fallback(깨진 이미지 주소로 initials: 한글 두 글자, 영문 두 글자, 한 글자, 긴 fallback), Composition(이름 + 소속)을 한 페이지에 둔다.
|
||||
- verification: `Image`, `Fallback`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- Size(24/32/40)와 fallback 색은 현재 HM 규격이며 Dashboard/GNB의 실제 사용이 정해지면 재검토.
|
||||
- 긴 fallback(예: 10자)은 가운데에서 잘린다. 1~2자 initials가 전제이며 줄임 규칙은 만들지 않았다.
|
||||
- Icon fallback(사람 아이콘) 규칙, 상태 badge, 겹침 그룹은 정의하지 않았다.
|
||||
- 이미지 비율이 다른 원본은 중앙 crop(`object-cover`)만 한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/avatar.tsx
|
||||
packages/ui/src/theme/avatar.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/avatar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더(Browser pane): 로드되는 이미지 Avatar는 `img`만 있고 fallback 요소가 없음(24/32/40px, radius 9999px). 깨진 이미지 주소의 Avatar는 `img` 없이 fallback이 렌더되고 `김한`/`HM`/`한`/`ABCDEFGHIJ`가 32×32 안에서 레이아웃 변동 없이 표시, 배경 `rgb(189,223,212)`·글자 `rgb(18,51,40)`·12px.
|
||||
- Responsive(390×844): Avatar 구성이 한 줄로 들어가고 가로 overflow 없음.
|
||||
- 미확인: 이미지가 느리게 로드될 때 fallback에서 image로 전환되는 순간의 깜빡임, `delayMs`, 대비 비율의 수치 측정.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Badge
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Badge 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Badge (CVA, `asChild` Slot) |
|
||||
| 대표 Story | `Components/Data & Display/Badge` → `All` (이름 "Badge") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Badge 문서가 없다(Border 1px 등 일부 언급만). 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: shadcn Badge 구조(CVA `variant`, `data-slot`, `asChild`, `cn(..., className)`)를 유지하고 `data-variant`로 theme CSS를 선택. Button variant를 재사용하지 않는다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 12 / 18 / 500 |
|
||||
| Padding | `2px 8px` (높이 24px) |
|
||||
| Border / Radius | 1px / `--hm-radius-control-md` (4px) |
|
||||
| Gap | shadcn `gap-1`, 아이콘 12px (`size-3`) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| variant | 면 | 글자 | 테두리 |
|
||||
|---|---|---|---|
|
||||
| `default` | `--hm-sem-action-primary` (Primary Green `#123328`) | `--hm-sem-on-action-primary` (White) | 면과 동일 |
|
||||
| `secondary` | `--hm-sem-bg-subtle` | `--hm-sem-fg-secondary` | 면과 동일 |
|
||||
| `destructive` | `--hm-sem-action-destructive` | White | 면과 동일 |
|
||||
| `outline` | `--hm-sem-bg-surface` | `--hm-sem-fg-primary` | `--hm-sem-stroke-default` |
|
||||
|
||||
상호작용 상태 없음 (`asChild`로 링크/버튼에 감쌀 때 그 요소가 담당).
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Badge:
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" 기본 "default"
|
||||
asChild?: boolean
|
||||
```
|
||||
나머지는 `<span>` props. 출력 속성: `data-slot="badge"`, `data-variant`. `badgeVariants`도 export.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/badge.css` (`--hm-badge-*`): `radius`, `padding-x/y`, `border-width`, `font-size/line-height/font-weight`, variant별 `{default,secondary,destructive,outline}-{background,text,border-color}`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
CVA 구조, `asChild`(Radix Slot), `data-slot`, `cn(..., className)`, 아이콘 크기 규칙(`[&>svg]:size-3`).
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `rounded-full`·`px-2 py-0.5 text-xs`·focus/invalid ring·hover class 제거 → theme CSS(radius 4). variant 이름은 shadcn 4종을 유지하고 색만 HM으로 매핑.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `variants`, `icon`, `long`, `link`(asChild). verification: `Variants`, `Playground`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
2026-10-06 (Badge 원복): 직전 Active Semantic 후속에서 `default`를 Light 2(`--hm-sem-state-active`)로 바꾼 변경을 취소했다. `default`는 Strong Brand / Primary인 Primary Green(`--hm-sem-action-primary` / `--hm-sem-on-action-primary`)을 쓰며 Active Semantic 적용 대상이 아니다. Component Token 이름은 유지하고 참조만 되돌렸다. `secondary`·`outline`·`destructive`는 그대로다. Badge는 비인터랙티브 label이라 hover 면을 두지 않는다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- success / warning / information 같은 상태 variant: HM 상태 색이 정해지지 않아 만들지 않았다.
|
||||
- size 변형, 점(dot) badge: 미결정.
|
||||
- 수치(12/18/500, 2/8, radius 4)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/badge.tsx
|
||||
packages/ui/src/theme/badge.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Badge, badgeVariants)
|
||||
src/components/badge.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: 4 variant 모두 12px/18px/500, padding `2px 8px`, radius 4px, 높이 24px, 색 위 표와 일치, 아이콘 12px.
|
||||
- 미확인: `asChild` 링크 focus 표시, 사이드바 화면, 390px 폭.
|
||||
- 2026-10-06 Badge 원복: `default` 면·테두리 `rgb(18,51,40)`(`#123328`), 글자 `rgb(255,255,255)`. secondary·destructive·outline 값 변화 없음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Breadcrumb
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Breadcrumb 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Breadcrumb (Radix 없음, `nav > ol > li` + `Slot`) |
|
||||
| 대표 Story | `Components/Navigation/Breadcrumb` → `All` (이름 "Breadcrumb") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Breadcrumb이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary/secondary)로 새로 정했다.
|
||||
- **구현 방식**: shadcn Breadcrumb 구조 유지, theme CSS로 HM 스타일 연결. 페이지 안에 직접 놓이는 navigation primitive라 Portal·overlay가 없다.
|
||||
- **역할 범위**: 현재 위치와 상위 이동만 담당한다. 메뉴 시스템·자동 접기(`maxItems`)는 없다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Item 간격 | 8px(Item 안 gap, 목록 gap) |
|
||||
| Link | `--hm-sem-fg-secondary`, hover 시 `--hm-sem-fg-primary` + 밑줄(offset 4px) |
|
||||
| Current Page | `--hm-sem-fg-primary`, weight 700, 기본 커서 |
|
||||
| Separator | shadcn `ChevronRightIcon` 16px, `--hm-sem-fg-secondary` |
|
||||
| Ellipsis | 20px 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
||||
| 긴 경로 | `flex-wrap`으로 줄바꿈 (`break-words`) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover (Link) | 전경 primary + 밑줄 |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%`, radius 2px |
|
||||
| Current | `aria-current="page"`, `aria-disabled="true"`, `role="link"` (shadcn), 링크가 아닌 `span` |
|
||||
| Disabled | 없음(없는 상태를 만들지 않음) |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`.
|
||||
|
||||
- `BreadcrumbLink`: 기본 `a`, `asChild`로 router Link 등과 조합(특정 router에 의존하지 않음).
|
||||
- `BreadcrumbPage`: 현재 위치 표시용 `span`. 클릭할 수 없다.
|
||||
- `BreadcrumbSeparator`: 기본 chevron, `children`으로 교체(예: `/`). `aria-hidden`, `role="presentation"`.
|
||||
- `BreadcrumbEllipsis`: 표시용(`aria-hidden`, sr-only "More"). Dropdown과 자동 결합하지 않는다. 메뉴 연결은 소비 조합(Story 예시).
|
||||
- `variant`, `size`, `maxItems`는 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/breadcrumb.css` (`--hm-breadcrumb-*`).
|
||||
|
||||
`font-size`, `line-height`, `font-weight`, `gap`, `link-color`, `link-hover-color`, `page-color`, `page-font-weight`, `separator-color`, `separator-size`, `ellipsis-size`, `focus-radius`. Semantic만 참조하고 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`nav aria-label="breadcrumb"`, `ol`/`li` 구조, `BreadcrumbPage`의 ARIA, separator의 `aria-hidden`, `asChild`(Slot), chevron·`MoreHorizontal` 아이콘, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `text-sm text-muted-foreground gap-1.5 sm:gap-2.5`, Link `hover:text-foreground`, Page `font-normal text-foreground`, 아이콘 `size-3.5`/`size-9` class 제거 → theme CSS.
|
||||
- Current Page를 weight 700으로 강조(shadcn은 normal).
|
||||
- Link에 밑줄 hover 및 focus ring 추가(shadcn은 색 변화만).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Breadcrumb"): Basic(3 depth), Ellipsis(Dropdown Menu 조합), Long path, Custom link(asChild)와 custom separator를 한 페이지에 둔다.
|
||||
- verification: `Basic`, `Ellipsis`, `Long Path`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(14/20, gap 8px, Current weight 700, chevron 16px)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 긴 경로의 자동 접기(`maxItems`)와 모바일 축약 표현은 만들지 않았다. 지금은 줄바꿈만 한다.
|
||||
- Separator 색은 Link와 같은 secondary(별도 색 결정 없음).
|
||||
- Ellipsis는 표시용이다. 접힌 항목을 보여주는 메뉴 규칙은 소비 조합으로 남긴다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/breadcrumb.tsx
|
||||
packages/ui/src/theme/breadcrumb.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/breadcrumb.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Link는 `a`, Current Page는 `span`(`aria-current="page"`, `aria-disabled="true"`, 커서 default), Tab으로 Link 사이 이동하고 Current Page는 포커스 대상이 아님, Separator·Ellipsis `aria-hidden`.
|
||||
- computed style: Link `rgb(110,103,89)` 14/20, Current `rgb(10,34,29)` 700.
|
||||
- Responsive: 390×844에서 목록 폭 358px, 긴 경로(7 depth)는 4줄(76px)로 줄바꿈되고 가로 overflow 없음.
|
||||
- 미확인: 실제 pointer hover/focus ring 시각, Ellipsis 안 Dropdown Menu 키보드 흐름.
|
||||
@@ -0,0 +1,191 @@
|
||||
# Button
|
||||
|
||||
현재 HM 구현과 Runtime-neutral 사용 기준 문서. 공통 원칙은 `AGENTS.md`와 `docs/guidelines/runtime-portability.md`를 따른다. 이 문서는 Button API·Token·상태·사용 위치·조합 계약을 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-*` class 없음) |
|
||||
| 기준일 | 2026-10-08 (Role × Appearance 계약·현재 Story 대조) |
|
||||
| 기반 | shadcn/ui Button (CVA, Radix `Slot`) |
|
||||
| 대표 Story | `Components/Form/Button` → `All` (이름 "Button") |
|
||||
| 보류 | Brand label Text Border 두께(EXPERIMENTAL, 미적용), Focus(ui_viz TBD) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size, padding-x, label size/line-height(button.md §26, D-087), Brand gradient·bevel(D-087), Standard Solid/Outline 색(§28, D-086, D-087), Hover/Pressed 동일 Black 20%(§12), Red Outline Hover/Pressed Red 8%.
|
||||
- **HM 사용자 확정 규칙**: 실제 화면 Brand 최대 1개·팝업 Brand 금지, 일반 확인/작성 Solid 세트, 삭제 확인·행/반복 작업 Outline 세트, 같은 세트의 appearance와 size 통일(§5). Disabled `0.6`은 ui_viz 규격 값이며, Input·Select의 2026-10-02 결정과는 별개다.
|
||||
- **shadcn 유지**: 구조(CVA, `data-slot`, `cn(..., className)`), focus/invalid ring class, `ghost`·`link` variant와 기존 size 이름.
|
||||
- **미정의/TBD**: Focus 표현(ui_viz TBD → shadcn 기본 focus ring 유지), `aria-invalid` 표현(ui_viz에 없음 → shadcn class 유지), `ghost`·`link`의 규격(ui_viz 없음).
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | Height(min) | Padding-x | Font / Line-height |
|
||||
|---|---|---|---|
|
||||
| `sm` | 28 | 10 | 12 / 18 |
|
||||
| `default` | 34 | 14 | 13 / 20 |
|
||||
| `lg` | 42 | 18 | 15 / 22 |
|
||||
|
||||
- 높이는 `min-height`다. 기본 라벨은 `nowrap`으로 한 줄을 유지한다. 여러 줄이 필요한 경우 기존 `className="whitespace-normal"`을 명시하며, 이때만 줄바꿈에 따라 높이가 늘어난다(가운데 정렬, `overflow-wrap: anywhere`).
|
||||
- 세로 padding은 `(height − line-height) / 2 − border`로 계산한다.
|
||||
- Radius `--hm-radius-button`, font-family `--hm-font-family-base`.
|
||||
- Weight: Brand 700, Standard 500.
|
||||
- 아이콘 전용 size(`icon*`)와 `xs`는 shadcn 기본이며 HM 규격이 없다(`data-appearance`가 있는 버튼에 한해 `default|sm|lg`만 HM 크기 적용).
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | treatment별 face/border (아래 Token) | ui_viz Confirmed |
|
||||
| Hover / Pressed (Brand, Solid) | face 위에 Black 20% overlay (`inset` shadow) | ui_viz §12 Confirmed |
|
||||
| Hover (Outline) | role ink로 채움 + 흰 글자 | ui_viz Confirmed |
|
||||
| Pressed (Outline) | 흰 face + Black 20% overlay | ui_viz Confirmed |
|
||||
| Red Outline Hover/Pressed | Red 8% face, Black overlay 없음 | ui_viz D-087 Confirmed |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed`, Default paint 유지 | ui_viz Confirmed (HM roles) |
|
||||
| Focus | shadcn 기본 `focus-visible` ring | ui_viz TBD → shadcn 유지 |
|
||||
| Invalid | shadcn `aria-invalid` ring | ui_viz 미정의 → shadcn 유지 |
|
||||
|
||||
- Brand/Solid의 Hover·Pressed는 `:not(:disabled)`에만 적용된다.
|
||||
- transition과 눌림 이동(`translate-y-px`)은 HM role에서 제거한다.
|
||||
- **`ghost`·`link`의 Disabled는 `opacity-50`(shadcn 기본)** 이다. HM role(`0.6`)과 값이 다르다. 규격이 없는 호환 variant라 그대로 둔다(아래 §11).
|
||||
|
||||
## 5. 사용 위치와 버튼 세트
|
||||
|
||||
- `brand`: 페이지 대표 실행에 사용한다. 실제 서비스 화면당 최대 1개이며 팝업에서는 사용하지 않는다. Storybook 비교 갤러리는 예외다.
|
||||
- `solid`: 일반 실행과 등록·저장·확인에 사용한다.
|
||||
- `outline`: 반복·보조 작업과 표/목록 행 작업에 사용한다.
|
||||
- 일반 작성·수정창: 취소 Solid + 등록/저장 Solid.
|
||||
- 일반 확인창: 취소 Solid + 확인 Solid.
|
||||
- 삭제 확인창: 취소 Outline + 삭제 Outline.
|
||||
- 표/목록 행과 반복·보조 작업: 동일 `appearance`의 Outline 조합.
|
||||
- FilterBar 대표 Story의 초기화·조회: `cancel` + `primary`, 둘 다 Outline·`size="default"`. 목록의 반복 조회·보조 작업 세트다.
|
||||
- 하나의 세트는 `appearance`와 `size`를 통일한다. 역할 차이는 variant의 기존 역할색과 문구로 표현한다.
|
||||
|
||||
사용 위치를 기준으로 appearance를 명시하며 버튼 문구만으로 판단하지 않는다. 컴포넌트는 appearance 생략 시 기본 `brand`를 출력하므로, 팝업·버튼 세트에서는 기본값에 의존하지 않는다.
|
||||
|
||||
## 6. API
|
||||
|
||||
```
|
||||
variant: shadcn "default | outline | secondary | ghost | destructive | link"
|
||||
→
|
||||
HM "primary | secondary | modify | destructive | cancel"
|
||||
+ 별칭 "default"(= primary), "outline"(= primary + outline appearance)
|
||||
+ 호환 "ghost", "link"
|
||||
appearance: (신규) "brand | solid | outline" 기본 "brand"
|
||||
size: "default | xs | sm | lg | icon | icon-xs | icon-sm | icon-lg" (shadcn 그대로)
|
||||
asChild: shadcn 그대로
|
||||
```
|
||||
|
||||
- 기본값: `variant="primary"`, `appearance="brand"`, `size="default"`.
|
||||
- 출력 속성: `data-slot="button"`, `data-variant`(별칭은 role로 정규화), `data-appearance`(`ghost`/`link`은 없음), `data-size`.
|
||||
- `secondary`의 의미가 바뀌었다: shadcn 회색 보조 → ui_viz Brown 역할. 기존 사용처 영향은 별도 확인하지 않았다.
|
||||
|
||||
### 6.1 허용 Role × Appearance (AI Reference)
|
||||
|
||||
현재 `button.css`의 역할별 Mapping과 대표 `All` Story의 `rolesFor`가 지원하는 조합은 다음과 같다.
|
||||
|
||||
- `primary` (Green): `brand`, `solid`, `outline`.
|
||||
- `secondary` (Brown): `brand`, `solid`, `outline`.
|
||||
- `modify` (Orange): `brand`, `solid`. **`outline` 미지원**.
|
||||
- `cancel` (Gray): `solid`, `outline`. **`brand` 미지원**.
|
||||
- `destructive` (Red): `outline`만 지원. **`brand`, `solid` 미지원**.
|
||||
|
||||
공개 Props는 `variant`와 `appearance`를 독립적으로 받는다. TypeScript 통과나 Storybook Controls에서 선택 가능하다는 사실은 해당 조합의 디자인 지원을 뜻하지 않는다. 미지원 조합에는 필요한 역할별 CSS Custom Property가 없으며 자동 보정·fallback도 없다. 기존 CSS 변수에 임의 fallback이나 새로운 역할색을 추가하지 않는다.
|
||||
|
||||
### 6.2 기본값·별칭·합성 주의
|
||||
|
||||
- `<Button>`은 Primary Brand다. `variant="destructive"`만 지정하면 미지원 Destructive Brand, `variant="cancel"`만 지정하면 미지원 Cancel Brand가 된다. 각각 허용 appearance를 명시한다.
|
||||
- `variant="default"`는 Primary 별칭이며 appearance 생략 시 Brand다. `variant="outline"`는 Primary Outline 별칭이며 전달한 appearance보다 우선한다. Red Outline은 `variant="destructive" appearance="outline"`로 작성한다.
|
||||
- `ghost`/`link`는 HM Role × Appearance 계약 밖의 shadcn 호환 variant다. `data-appearance`를 출력하지 않으며 appearance를 전달해도 HM treatment를 적용하지 않는다.
|
||||
- `AlertDialogAction`과 `AlertDialogCancel`의 기본 appearance는 Button과 달리 `solid`다. `<AlertDialogAction variant="destructive">`만 지정하면 미지원 Destructive Solid다. 삭제 확인은 Action과 Cancel **모두** `appearance="outline"`을 명시한다. 자세한 합성 API는 [Alert Dialog](alert-dialog.md#5-api)를 따른다.
|
||||
|
||||
```tsx
|
||||
// 삭제·취소: 같은 appearance와 size (DialogClose asChild로도 합성 가능)
|
||||
<Button variant="cancel" appearance="outline" size="default">취소</Button>
|
||||
<Button variant="destructive" appearance="outline" size="default">삭제</Button>
|
||||
|
||||
// Alert Dialog 삭제 확인: 두 요소 모두 Outline을 명시
|
||||
<AlertDialogCancel appearance="outline" size="default">취소</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" appearance="outline" size="default">삭제</AlertDialogAction>
|
||||
```
|
||||
|
||||
미지원 조합 추가, role별 자동 appearance 선택, 기본 appearance 변경은 사용자 결정이 필요한 별도 변경이다. 현재 계약과 공개 API를 유지한다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/button.css` (`--hm-button-*`). 공통 값은 `packages/ui/src/theme/default.css`, Primitive는 `packages/ui/src/theme/primitives.css`.
|
||||
|
||||
- 공통 Semantic 재사용: `--hm-radius-button`, `--hm-font-family-base`, `--hm-font-weight-*`, `--hm-color-text-inverse`, `--hm-color-surface-default`, `--hm-color-action-*`, `--hm-sem-surface/border-action-primary`.
|
||||
- `--hm-button-height-md`는 `--hm-control-height-md`(34)를 참조한다. sm/lg 높이는 Button 전용 값이다.
|
||||
- Brand/Solid의 색 Token은 role별(`green|brown|orange|gray`)로 Component 안에 둔다. Brown/Orange gradient의 일부 stop과 Gray 값은 `default.css`의 공개 Theme palette를 참조한다. `button.css`에 직접 HEX는 남아 있지 않다.
|
||||
- Input·Select와 높이 값(28/34/42)이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이기 때문이다.
|
||||
|
||||
## 8. Runtime-neutral DOM 계약
|
||||
|
||||
React가 아닌 Runtime도 배포된 `@hm-design/ui/theme.css` 또는 승인된 standalone Theme CSS를 먼저 로드하고, semantic `<button>`에 다음 속성을 사용한다.
|
||||
|
||||
- `data-slot="button"`, `data-variant="primary|secondary|modify|destructive|cancel"`, `data-appearance="brand|solid|outline"`, `data-size="default|sm|lg"`.
|
||||
- Role × Appearance는 §6.1의 허용 조합만 사용한다. 위 속성 vocabulary가 모든 곱집합을 허용하는 것은 아니다.
|
||||
- CSS는 `--hm-button-*` Component variable과 `--hm-sem-*`/기존 Theme variable을 소비한다. HEX와 임의 variable을 추가하지 않는다.
|
||||
- native `disabled`, `:hover`, `:active`, `:focus-visible`로 Disabled/Hover/Pressed/Focus 계약을 유지한다. `aria-disabled`만으로 native disabled를 대체하지 않는다.
|
||||
- 팝업의 취소/확인 조합은 같은 `size`를 사용하고, 삭제 확인은 취소·삭제 모두 Outline이어야 한다. Escape, Tab 순서, 초기 포커스와 닫힌 뒤 trigger 포커스 복귀는 modal primitive 또는 동등한 keyboard contract로 보존한다.
|
||||
|
||||
React의 `variant` prop을 그대로 복제할 필요는 없지만, 동일한 role/treatment attribute와 시각·상태·접근성 계약을 유지해야 한다.
|
||||
|
||||
## 9. shadcn / Radix에서 유지한 항목
|
||||
|
||||
CVA 구조, `data-slot`, `asChild`(Radix `Slot`), `className` 확장, focus-visible/invalid ring class, `[&_svg]` 아이콘 규칙, 기존 size 이름, `ghost`·`link` variant.
|
||||
|
||||
## 10. HM에서 변경한 항목
|
||||
|
||||
- `variant`를 ui_viz 역할로 재정의하고 `appearance` 추가.
|
||||
- HM role의 transition·눌림 이동 제거, `disabled:cursor-not-allowed`.
|
||||
- 높이를 `min-height` + 계산 padding으로 구성. 일반 라벨은 한 줄 유지하며, Long Label specimen은 명시적 `whitespace-normal`로 줄바꿈 지원.
|
||||
- shadow에 shadcn ring shadow(`--tw-ring-*`)를 끝에 포함해 bevel과 ring이 함께 보이게 함(지침 §6.7).
|
||||
- `hm-*` class 제거.
|
||||
|
||||
## 11. Storybook
|
||||
|
||||
- 대표 Story `All`("Button")이 기본 sidebar의 유일한 Button 항목이다 → 1뎁스.
|
||||
- `All`의 구역: Overview → Sizes → Variants → States. 기본 버튼·크기·허용 조합·Brand/Standard 상태를 보여준다. Red Outline은 Variants와 Standard States에도 포함되며, 삭제·취소 세트와 Long Label은 verification Story에서 따로 확인한다.
|
||||
- verification(`tags: ["verification"]`, 기본 sidebar 제외): `Roles`, `Sizes`, `States`("Brand States"), `StandardStates`, `LongLabel`, `KoreanLabels`, `IconTextNarrow`, `Destructive`, `Playground`(HM 전용), `Compatibility`(ghost/link, default/outline 별칭). Controls는 조합별 선택을 제한하지 않으므로 §6.1의 허용 조합을 직접 지킨다. Verification은 기술 점검용이며 운영 화면의 사용 위치·세트 규칙은 §5가 우선한다.
|
||||
- 대표 경로/ID: `Components/Form/Button → All` (`components-form-button--all`), `Components/Overlay/Dialog → All` (`components-overlay-dialog--all`), `Components/Overlay/Alert Dialog → All` (`components-overlay-alert-dialog--all`), `Patterns/Data/Filter Bar → Filter Bar` (`patterns-data-filter-bar--all`). 삭제 세트 점검 ID는 `components-form-button--destructive`(verification)다.
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
|
||||
## 12. 사용자 확정 결정
|
||||
|
||||
사용자가 확정한 사용 위치·세트 규칙은 §5에 기록한다. 2026-10-08 정합성 점검에서는 기존 허용 조합과 기본값을 유지하고 미지원 조합을 새로 지원하지 않는다.
|
||||
|
||||
## 13. 미확정 / 보류
|
||||
|
||||
- **EXPERIMENTAL**: Brand label Text Border 두께 — 적용하지 않음.
|
||||
- **TBD(ui_viz)**: Focus 표현.
|
||||
- **ui_viz 미정의**: `aria-invalid`, `ghost`/`link`, `xs`·`icon*` size, `ghost`/`link` Disabled(현재 0.5, shadcn 기본).
|
||||
- Disabled `0.6`은 Input·Select의 사용자 결정과 별개이며, 전역 Disabled Token이 아니다.
|
||||
|
||||
## 14. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/button.tsx
|
||||
packages/ui/src/theme/button.css
|
||||
src/components/button.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 15. 검증 상태
|
||||
|
||||
### 2026-10-08 Button 조합 정합성 점검
|
||||
|
||||
- 기존 6024 Storybook 재사용. 대표 Button의 허용 조합·고정 Hover/Pressed·Disabled를 computed style로 확인: 높이 28/34/42px, Disabled opacity `0.6`, Solid Hover/Pressed Black 20%, Red Outline Hover/Pressed Red 8%와 Red 전경 유지. 삭제 버튼의 실제 포인터 Hover도 같은 면을 확인했다.
|
||||
- Playground에서 Destructive Brand/Solid와 Modify Outline을 직접 렌더했다. Destructive Brand/Solid는 역할 배경 변수가 비어 투명 배경·흰 글자, Modify Outline은 역할 ink 변수가 비어 기본 전경색으로 표시된다. 디자인 계약상 미지원이며 이 점검에서 지원을 추가하지 않았다.
|
||||
- 대표 Dialog / Alert Dialog: 일반 확인 Solid 세트·삭제 확인 Outline 세트, 동일 `size="default"`·높이 34px 확인. AlertDialog Action/Cancel의 `data-slot="button"`과 역할·appearance 합성이 유지된다. Dialog의 Escape, Alert Dialog의 취소·Escape 닫기 후 trigger 포커스 복귀, Alert Dialog 초기 Cancel focus·Tab 순환 확인.
|
||||
- 대표 Filter Bar: 초기화 Cancel Outline / 조회 Primary Outline·동일 크기 확인. 검색 조건 입력 후 초기화로 빈 검색어·조건 0개 복원. 조회는 기존 callback 없는 조합 예시다.
|
||||
- 관련 브라우저 Console warning/error 없음. `npm run check:theme-defaults` 통과. Component·CSS·Theme 값·공개 API 변경 없음. 전체 회귀·build·브라우저별 비교는 요청 범위에 따라 생략했다.
|
||||
|
||||
### 과거 검증 기록
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02, 복구 후).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교: 대표·verification Story의 버튼 51개(+대표 49개) computed style 일치(Red Outline 8% 면은 흰 바탕 합성값으로 같은 RGB, `cursor`는 ui_viz도 Web Mapping으로 분류해 shadcn 기본 유지). 대표 Story를 ui_viz 4구역으로 복원(Story만 변경).
|
||||
- 과거 Storybook 기록: Long Label·Destructive 및 verification Story ID 렌더 확인. 현재 `All` 구역 구성은 §11을 따른다.
|
||||
- 당시 미확인: 실제 사이드바 화면, Disabled computed opacity(Button), 브라우저별 렌더, `secondary` 의미 변경의 기존 사용처 영향. Disabled computed opacity는 위 2026-10-08 점검에서 확인했다.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Calendar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Calendar 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Primitive component** (날짜 선택 UI). Date Picker 자체가 아니다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Calendar = **react-day-picker v10** |
|
||||
| 대표 Story | `Components/Search & Date/Calendar` → `All` (이름 "Calendar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Calendar가 없다(인벤토리에 Date Picker 이름만). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Button/Input/Popover Token은 참조하지 않는다(Navigation 버튼이 Button처럼 보여도 Calendar Token으로 관리).
|
||||
- **구현 방식**: react-day-picker가 날짜 계산, 월 이동, 키보드, 선택 모드, ARIA grid를 소유한다. HM은 시각만 입힌다. 라이브러리의 기본 class(`rdp-*`)와 날짜 셀의 `data-*` 상태 속성을 그대로 두고 `[data-slot="calendar"]` 아래에서 CSS로 스타일한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | 없음(투명). 감싸는 컨테이너(Popover, Card 등)가 면과 윤곽을 가진다 |
|
||||
| Padding / 폭 | 12px / 내용 폭(`fit-content`, 한 달 276px) |
|
||||
| Caption | 가운데, 높이 32px, 14 / 20 / 700 |
|
||||
| Navigation 버튼 | 28 × 28px, radius 4px, 아이콘 16px(lucide chevron), Caption 줄 양끝에 절대 배치 |
|
||||
| Weekday | 12 / 18 / 500, `--hm-sem-fg-secondary`, 폭 36px |
|
||||
| Day | 36 × 36px 셀과 버튼, radius 4px, 14 / 20 |
|
||||
| 여러 달 | `numberOfMonths`에 따라 flex로 나란히, 좁으면 줄바꿈(gap 16px) |
|
||||
| Locale | 기본 **한국어**(`react-day-picker/locale`의 `ko`, 추가 dependency 없음). `locale` prop으로 교체 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 투명 |
|
||||
| Hover | 배경 `--accent` 45% |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` |
|
||||
| Today (`data-today`) | 배경 `--hm-sem-bg-subtle`, weight 700 |
|
||||
| Selected (`data-selected`) | 배경 `--hm-sem-state-active`(Light 2 `#266651`), 글자 `--hm-sem-on-state-active`(흰색) (hover해도 유지, focus 시 fill + Focus ring) |
|
||||
| Outside (`data-outside`) | 글자 `--hm-sem-fg-secondary`, opacity 0.6 |
|
||||
| Disabled (`data-disabled`) | opacity 0.4(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 선택 불가 |
|
||||
| Range start / middle / end (`rdp-range_*`) | start·end는 Active 면(`--hm-calendar-range-edge-*` → `--hm-sem-state-active`, hover해도 유지, 안쪽 모서리 직각), middle은 `--accent` 45% 면 + 직각 |
|
||||
| Nav 버튼 disabled | opacity 0.4 (`startMonth`/`endMonth` 경계) |
|
||||
|
||||
기반 라이브러리가 실제로 제공하는 상태만 쓴다. HM 전용 상태 속성은 없다.
|
||||
|
||||
## 5. API
|
||||
|
||||
react-day-picker `DayPicker` props 그대로. export: `Calendar`.
|
||||
|
||||
- `mode` `"single" | "multiple" | "range"`, `selected`, `onSelect`, `required`, `defaultMonth`, `month`/`onMonthChange`, `disabled`(Matcher), `startMonth`/`endMonth`, `numberOfMonths`, `captionLayout`, `weekStartsOn`, `today`, `locale` 등.
|
||||
- Calendar가 바꾸는 기본값: `showOutsideDays=true`, `locale=ko`, Chevron 아이콘을 lucide로 교체(`components.Chevron`).
|
||||
- 출력 `data-slot="calendar"`. 날짜 셀은 `td.rdp-day`(`data-selected`, `data-today`, `data-outside`, `data-disabled`, `data-focused`, `aria-selected`).
|
||||
- `variant`, `size`는 만들지 않았다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `react-day-picker` ^10.0.2 (`packages/ui`에 추가, shadcn Calendar의 기반). 날짜 계산용으로 `date-fns`와 `@date-fns/tz`가 이 패키지의 dependency로 따라 들어온다. HM이 직접 date 라이브러리를 추가하지 않았다.
|
||||
- locale은 같은 패키지의 `react-day-picker/locale`(`ko`)을 사용(추가 설치 없음).
|
||||
- CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/calendar.css` (`--hm-calendar-*`).
|
||||
|
||||
`padding`, `text`, `month-gap`, `caption-*`, `nav-size|radius|icon-size|hover-background`, `weekday-*`, `day-size|radius|font-size|line-height|hover-background`, `today-background|font-weight`, `selected-background|text`, `range-edge-background|text`, `range-middle-background|text`, `outside-color|opacity`, `disabled-opacity`. Semantic/Theme palette를 참조한다. Selected·Range edge는 `--hm-sem-state-active` / `--hm-sem-on-state-active`(2026-10-06 Active Semantic)를 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 8. react-day-picker에서 유지한 항목
|
||||
|
||||
날짜 계산, 월 이동, 키보드 모델(방향키, Home/End, PageUp/PageDown), 선택 모드와 `required`, disabled/outside/hidden 날짜, 숫자·날짜 접근성 label(locale 기반), `role="grid"`/`gridcell`, `aria-selected`, roving tabindex.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Calendar"): Single(오늘 10/2, 선택 10/15), Disabled dates(5일 이전·일요일), Range(10/8–10/14), Two months를 한 페이지에 둔다. `today`는 라이브러리 prop으로 2026-10-02에 고정(어느 날 열어도 같게 보이기 위함).
|
||||
- 2026-10-06 Visual QA: Two months 구역만 `StateBoard`의 `wide`를 사용한다. Desktop에서 252px month 두 개 + 16px gap + Calendar padding 24px + frame border 2px가 들어가며 좁은 폭에서는 기본 wrap을 유지한다. Production Calendar API/CSS는 변경하지 않았다.
|
||||
- verification: `Single`, `Disabled Dates`, `Range`.
|
||||
- meta에 `component`를 지정하지 않는다(props가 선택 모드 union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(36px 셀, 12px padding, Today 면, Selected 면)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 기본 locale을 한국어로 둔 것은 현재 HM 서비스 기준이며, 다국어는 소비자가 `locale`을 넘긴다. i18n 시스템은 만들지 않았다.
|
||||
- 주 시작 요일은 locale 기본(한국어는 일요일).
|
||||
- `captionLayout="dropdown"`(월/연도 select)의 시각은 스타일링하지 않았다(`.rdp-dropdown*`). 사용이 정해지면 추가.
|
||||
- 주 번호(`showWeekNumber`), footer, 달력 애니메이션(`animate`)은 스타일링하지 않았다.
|
||||
- **모바일**: 한 달은 276px로 390px 폭에 들어간다. `numberOfMonths > 1`은 좁은 화면에서 줄바꿈으로 세로로 쌓이며 별도 responsive calendar는 만들지 않았다.
|
||||
- Range의 hover 미리보기, 최소/최대 길이 표현은 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/calendar.tsx
|
||||
packages/ui/src/theme/calendar.css
|
||||
packages/ui/package.json (dependency: react-day-picker)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/calendar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Visual QA: Two months는 1280/1024/768px 한 행, 390px wrap, 모든 month 폭 252px 및 페이지 가로 overflow 없음. Production CSS 변경 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더/DOM(Browser pane): 한국어 caption "2026년 10월", 요일 "일월화수목금토", `role="grid"`, 날짜 폭 36px, Selected `rgb(18,51,40)`·흰 글자, Today `rgb(244,241,234)`·700, 이전/다음 달 버튼 label이 한국어("이전 달로 이동"/"다음 달로 이동").
|
||||
- pointer: 날짜 클릭으로 선택(`aria-selected=true`), 다음/이전 달 버튼으로 월 이동(10월 → 11월, 10월 → 9월).
|
||||
- keyboard: 포커스 후 ArrowRight 10 → 11, ArrowDown 11 → 18(다음 주), Enter와 Space로 선택, PageDown으로 다음 달(11월), Home으로 주의 첫 날. roving tabindex가 선택된 날짜에 있음.
|
||||
- disabled: 5일 이전과 일요일은 `data-disabled`, 버튼 `disabled`, opacity 0.4, 클릭해도 선택 안 됨. 가능한 날짜는 선택됨.
|
||||
- range: 시작/끝은 Active 면(안쪽 직각), 중간은 accent 45% 면(모서리 0)으로 구분됨(처음에는 `data-selected` 규칙이 중간 면을 덮는 버그가 있어 specificity를 올려 수정).
|
||||
- Responsive(390×844): Calendar 폭 276px(두 달은 줄바꿈되어 356px), 페이지 가로 overflow 없음.
|
||||
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, `multiple` 모드, `captionLayout="dropdown"`, RTL, 타임존(`@date-fns/tz`) 동작.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Card
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Card 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
> 아래 외부 ui_viz 문서는 ZIP 미포함 초기 출처이며 실행·구현의 필수 의존성이 아니다. 현재 기준은 이 문서와 HM 구현·대표 Story다. ui_viz에는 `docs/components/card.md`가 없다. Card 규격은 `docs/patterns/content-container.md`(Type B **Content Card**)와 `docs/patterns/content-header.md`(`content` profile)가 소유한다. 두 문서는 "범용 Card System이 아니다"라고 명시하며, 상태·Footer·Media·elevation을 만들지 않았다(Out of Scope). HM Card는 이 Type B를 shadcn Card에 적용한 것이다. Type A(Section Panel)는 구현하지 않았다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`<Card />`만으로 적용, `hm-card` class 없음) |
|
||||
| 기준일 | 2026-10-02 (문서·주석 정합성 정정 2026-10-08, 디자인 값 변경 없음) |
|
||||
| 기반 | shadcn/ui Card (div 기반, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Card` → `All` (이름 "Card") |
|
||||
| 보류 | Eyebrow, Divider, Footer·Media, 반응형, 폭 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: 컨테이너 radius 4px(`radius.container.default`), 표면 White(`color.bg.surface`)/Beige(`color.bg.content`), 제목 전경 `color.fg.primary`.
|
||||
- **ui_viz 채택(SOURCE_ADOPTED, Canonical Pattern Q6 `YES`, 2026-09-28)**: border 1px, body-padding 24px 사방, header↔body 간격 24px(D-093 D-042/D-059), title↔action 간격 20px, 교차축 중앙 정렬, 상태 없음. 이 값들은 문서상 `Confirmed` 표기가 아니라 owner가 채택한 Canonical 값이다. HM Card는 이 채택 값을 적용한다.
|
||||
- **ui_viz LOCAL_EXCEPTION(컴포넌트 범위 값)**: border 색 `#DEDBD2`(전역 token 아님, Tabs·Search Field와 값만 같고 alias 금지), Beige border `#ECD4A2`(Primitive `hm-brown-light-4`와 같은 값). **현재 HM 구현은 이 두 값을 컴포넌트 전용 값으로 두지 않고 공유 Semantic Token에 연결한다.** 차이는 §6.1에 기록한다.
|
||||
- **ui_viz CANONICAL_OVERRIDE**: Title Source 19px/700 → `heading-md` 20/28/600.
|
||||
- **HM 사용자 결정**: 없음.
|
||||
- **shadcn 유지**: 아래 §7.
|
||||
- **ui_viz 미정의**: Description, Footer, `size="sm"`, Media, Hover/Selected/Clickable, shadow.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Background | White (`--hm-sem-bg-surface`) / Beige (`--hm-sem-bg-content`) | Confirmed |
|
||||
| Border | 1px solid `--hm-sem-stroke-input-default`(Default `#dedbd2`) / Beige는 `--hm-sem-palette-brown-light-4`(Default `#ecd4a2`) | 두께는 채택. 색 값은 LOCAL_EXCEPTION 값과 같고 Token 연결은 §6.1 |
|
||||
| Radius | 4px | Confirmed |
|
||||
| Shadow | 없음 | ui_viz: "Source의 컨테이너는 그림자를 쓰지 않는다" |
|
||||
| Padding | 24px 사방 (Card 위/아래, Header·Content 좌우) | 채택 |
|
||||
| Header ↔ Content 간격 | 24px | 채택 (D-093) |
|
||||
| 자식 clipping | 하지 않음 (`overflow: visible`) | 채택 (Type B) |
|
||||
| Title | 20px / 28px / 600, `--hm-sem-fg-primary` | CANONICAL_OVERRIDE |
|
||||
| Header 안 Title ↔ Action 간격 | 20px (`column-gap`) | 채택 |
|
||||
| Action 정렬 | Title 반대쪽 끝, 교차축 중앙 | 채택 |
|
||||
| Description | shadcn 기본 (`text-sm text-muted-foreground`), HM Card CSS 미적용. 사용 범위는 §5.1 | ui_viz 미정의 |
|
||||
| 폭/높이 | 내용과 레이아웃이 결정. 고정 token 없음 | ui_viz: "Card width token을 만들지 않는다" |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
- 존재하는 상태는 **Default 하나**다. Hover / Focus / Pressed / Selected / Disabled는 ui_viz가 `NOT_APPLICABLE`로 명시했고 추가하지 않았다. Clickable·Interactive 변형도 없다.
|
||||
- Card 안의 Button·Input 등은 각자의 컴포넌트 문서에 따른 상태를 가진다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Card:
|
||||
size?: "default" | "sm" (shadcn 그대로)
|
||||
→
|
||||
size?: "default" | "sm" (유지)
|
||||
surface?: "surface" | "content" (신규) 기본 "surface" — ui_viz surface 축 (White / Beige)
|
||||
|
||||
CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter: shadcn API 그대로
|
||||
```
|
||||
|
||||
- 출력 속성: `data-slot="card"`, `data-size`, `data-surface`.
|
||||
- 새 prop은 `surface` 하나다. ui_viz가 Content Card의 표면 축(`surface`/`content`)을 정의했기 때문이다. `variant`·`interactive`·`selected` 등은 근거가 없어 만들지 않았다.
|
||||
- Eyebrow는 shadcn Card에 대응 요소가 없다. 필요하면 소비자가 조합한다(미구현, §11).
|
||||
|
||||
### 5.1 CardDescription 사용 범위
|
||||
|
||||
- 공개 API로 export되며(`packages/ui/src/index.ts`) shadcn 구현 그대로다. `packages/ui/src/theme/card.css`는 `card-description`을 선택하지 않으므로 HM 전용 Typography·색이 없다(`text-sm text-muted-foreground`).
|
||||
- ui_viz Content Card Anatomy에 Description이 없다. 대표 Story `All`에는 넣지 않으며, HM Card 규격 요소가 아니라 **shadcn 호환 요소**로만 둔다.
|
||||
- 현재 저장소의 실제 사용처:
|
||||
|
||||
| 위치 | 용도 | 비고 |
|
||||
|---|---|---|
|
||||
| `src/components/card.stories.tsx` `Compatibility` | shadcn 호환 확인 | verification 전용 |
|
||||
| `src/patterns/dashboard-layout.stories.tsx` | 업무 목록 건수(`role="status"`), KPI 라벨(Title 없이 Description만), 가로 스크롤 안내 | Pattern Story 안의 소비 예시. 별도 스타일 없음 |
|
||||
| `src/App.tsx` (Demo) | Panel 부제 | `.hm-panel-header`가 11px·`--hm-muted`로 덮어씀. Demo 전용(§8.1) |
|
||||
|
||||
- Header에 Description이 있으면 shadcn의 `has-data-[slot=card-description]:grid-rows-[auto_auto]`로 2행 grid가 되고, `CardAction`은 `row-span-2`로 두 행에 걸친다. 이 동작은 shadcn 그대로다.
|
||||
- Description의 HM Token·크기·색은 미정의다. 필요해지면 이 문서에서 결정한 뒤 Token으로 추가한다(§11).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/card.css` (`--hm-card-*`), `packages/ui/src/theme.css`에서 import.
|
||||
|
||||
- 재사용한 공통 값: `--hm-radius-container`(radius), `--hm-sem-bg-surface`, `--hm-sem-bg-content`, `--hm-sem-fg-primary`, `--hm-sem-stroke-input-default`(White border), `--hm-sem-palette-brown-light-4`(Beige border). Border 연결 상세는 §6.1.
|
||||
- Card 전용 값: `--hm-card-radius`, `--hm-card-border-width`(1px), `--hm-card-padding`(24px), `--hm-card-border-color`, `--hm-card-border-color-on-content`, `--hm-card-surface`, `--hm-card-surface-content`, `--hm-card-text`, `--hm-card-header-gap`(20px), `--hm-card-title-font-size|line-height|font-weight|color`.
|
||||
- **Primitive 신규 추가 1건**: `--hm-brown-light-4: #ecd4a2` (`packages/ui/src/theme/primitives.css`). ui_viz가 Confirmed Primitive `hm-brown-light-4`로 명시한 값이다. Card CSS는 이 Primitive를 직접 참조하지 않고 Semantic alias `--hm-sem-palette-brown-light-4`를 참조한다.
|
||||
- shadcn의 `--card-spacing` 변수를 24px(`--hm-card-padding`)로 채운다. `size="sm"`은 TSX class의 12px을 그대로 쓴다.
|
||||
- Input·Select·Button과 값(4px, `#dedbd2`)이 겹쳐도 그쪽 Component Token을 alias하지 않는다. `--hm-card-radius`는 `--hm-radius-container`를 참조하고 `--hm-radius-input`과는 연결하지 않는다(ui_viz가 alias 금지를 명시).
|
||||
- `--card-spacing`과 Title 규칙은 `:where()`로 감싸 specificity를 낮췄으므로 소비자 `className`과 Demo 스타일이 덮어쓸 수 있다. border·radius·surface 규칙(`[data-slot="card"]`, `[data-surface="content"]`)은 `:where()` 없는 attribute selector다.
|
||||
|
||||
### 6.1 Border Token 실제 연결 (LOCAL_EXCEPTION과의 차이)
|
||||
|
||||
| Card 변수 | 실제 참조 (`card.css`) | Runtime 계약 key (`contract.ts`) | HM Default 해석 경로 (`default.css` → `primitives.css`) | Default 값 |
|
||||
|---|---|---|---|---|
|
||||
| `--hm-card-border-color` | `var(--hm-sem-stroke-input-default)` | `color.stroke.input.default` | `--hm-color-border-control` → `--hm-sem-palette-warm-neutral-pale` → `--hm-warm-neutral-pale` | `#dedbd2` |
|
||||
| `--hm-card-border-color-on-content` | `var(--hm-sem-palette-brown-light-4)` | `color.palette.brown-light-4` | `--hm-brown-light-4` | `#ecd4a2` |
|
||||
|
||||
- **ui_viz 기술**: 두 border 색은 LOCAL_EXCEPTION 컴포넌트 값이며, 전역 token이 아니고 다른 컴포넌트 값과 alias하지 않는다.
|
||||
- **현재 구현**: Default 렌더 값은 ui_viz 값과 같다. 다만 White border는 Input·Select·Dialog·Separator 등이 함께 쓰는 공유 Semantic `--hm-sem-stroke-input-default`를, Beige border는 공유 Palette Semantic `--hm-sem-palette-brown-light-4`를 참조한다. 다른 Component Token(`--hm-input-*` 등)은 alias하지 않는다.
|
||||
- **결과**: 외부 Theme JSON이 `color.stroke.input.default` 또는 `color.palette.brown-light-4`를 override하면 Card border도 함께 바뀐다. Card만 따로 조정하려면 Component 변수 `--hm-card-border-color` / `--hm-card-border-color-on-content`를 덮어써야 하며, 이 변수는 Runtime 계약 key가 아니다.
|
||||
- 이 절은 문서 정정이다. Token 연결과 렌더 값은 바꾸지 않았다. Card 전용 border token으로 분리할지는 미결정이다(§11).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
semantic HTML(`div`), React composition(Header/Title/Description/Action/Content/Footer), `data-slot`, `cn(..., className)`, header의 grid 구조와 `has-data-[slot=card-action]` 2열 전환, Footer(`border-t`, `bg-muted/50`, 전체 폭), 첫/마지막 `img` 처리, `has-data-[slot=card-footer]:pb-0`, `size="sm"`, Description 스타일. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `rounded-xl`, `bg-card`, `ring-1 ring-foreground/10`, `overflow-hidden`, 기본 `--card-spacing: 16px`을 TSX에서 제거하고 theme CSS(border, radius 4px, surface, 24px)가 담당.
|
||||
- ring 대신 실제 1px border 사용, shadow 없음.
|
||||
- Title을 20/28/600으로 변경(`size="sm"` 제외).
|
||||
- Header 열 간격 20px, Action 교차축 중앙 정렬.
|
||||
- Footer 아래 모서리를 컨테이너 radius(border 안쪽 3px)에 맞춤. Footer 표면은 shadcn 그대로다.
|
||||
- `surface` prop 추가.
|
||||
|
||||
### 8.1 Demo `.hm-panel`과 운영 Card 구분
|
||||
|
||||
- **운영 Card**: `@hm-design/ui`의 `<Card />`와 `packages/ui/src/theme/card.css`. 이 문서 §3의 값(padding 24px, Title 20/28/600 등)과 대표 Story `All`이 기준이다. 소비 프로젝트는 `hm-panel*` class 없이 이 API만 쓴다.
|
||||
- **Demo Panel**: `src/App.tsx`의 4개 Card에 붙은 `hm-panel`, `hm-panel-header`, `hm-panel-icon`, `hm-panel-content` class와 `src/hm-theme.css` 규칙. 패키지(`packages/ui`)에 포함되지 않으며 Card 규격 Reference가 아니다.
|
||||
- `.hm-panel`은 Card 외형(border, radius, background, shadow)을 지정하지 않는다. 다만 Demo class 묶음은 운영 Card 값을 다음처럼 덮어쓴다.
|
||||
|
||||
| Demo class | 덮어쓰는 값 | 운영 Card 값 |
|
||||
|---|---|---|
|
||||
| `.hm-panel` | `gap: 0`, `--card-spacing: 16px` | gap·padding 24px |
|
||||
| `.hm-panel-header` | 아이콘 34px 열 grid, `column-gap: 11px`, `border-bottom: 1px solid #ede9df`, `padding: 22px 23px 17px` | Header 좌우 24px, column-gap 20px, divider 없음 |
|
||||
| `.hm-panel-header [data-slot="card-title"]` | 15px / 1.35 / 700, letter-spacing -.02em | 20 / 28 / 600 |
|
||||
| `.hm-panel-header [data-slot="card-description"]` | 11px, `--hm-muted`, margin-top 2px | shadcn 기본 `text-sm text-muted-foreground` |
|
||||
| `.hm-panel-icon` | 34px 아이콘 박스 | 해당 요소 없음 |
|
||||
| `.hm-panel-content` | `padding: 23px` | 좌우 24px |
|
||||
|
||||
- Demo 값은 HM Token이 아닌 hex 값을 포함한다. Card Token·Story의 근거로 쓰지 않으며, Demo 화면을 Card 디자인 Reference로 캡처·인용하지 않는다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Card")이 기본 sidebar의 유일한 Card 항목이다 → 1뎁스. 구역: `surface`(White), `content`(Beige), `action`(Title + Action), `form`(본문에 Input).
|
||||
- verification(`tags: ["verification"]`): `Surfaces`, `Action`, `Compatibility`(HM 전용: Description, Footer, `size="sm"`).
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
- 대표 Story에 Hover/Selected/Footer/Media는 넣지 않았다(ui_viz 미정의).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
추가 사용자 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Eyebrow**: ui_viz `content` header는 Eyebrow(11→12px/700, letter-spacing .12em, `#3E3523`)를 필수 요소로 둔다. shadcn Card에 대응 요소가 없고 이번 범위에 추가하지 않았다. 현재 Title만 있는 Header는 ui_viz Anatomy의 부분 구현이다.
|
||||
- **Divider(선택)**: ui_viz는 Header 뒤 24px + 1px + 18px의 선택 divider를 정의한다(`CONTENT_CARD_DIVIDER_OPTIONAL`). 기본은 없음이라 구현하지 않았다.
|
||||
- **Tabs Meta**: Meta가 Tabs일 때 Header↔Body 간격이 0이 되는 예외는 구현하지 않았다.
|
||||
- **ui_viz 미정의**: Description, Footer(`footer variant`는 Out of Scope), Media, `size="sm"`, 반응형, Card 폭.
|
||||
- **자식 clipping**: ui_viz Type B는 하지 않음이다. `overflow-hidden`을 제거했다. 대신 Footer 모서리를 CSS로 맞췄고 이미지 모서리는 shadcn `rounded-*`가 남아 있다.
|
||||
- **LOCAL_EXCEPTION**: ui_viz는 border 색 두 값을 전역 token이 아닌 컴포넌트 값으로 두고 tokenization을 follow-up으로 둔다. 현재 HM 구현은 공유 Semantic(`color.stroke.input.default`, `color.palette.brown-light-4`)에 연결되어 있다(§6.1). Card 전용 Semantic/Runtime key로 분리할지는 미결정이며, 이번 정정에서 Token 연결을 바꾸지 않았다.
|
||||
- **CardDescription HM 규격**: Typography·색·간격 미정의. shadcn 호환 요소로만 둔다(§5.1).
|
||||
- **Type A Section Panel**: 구현하지 않았고 미구현 상태를 유지한다. Demo `.hm-panel`은 Type A 구현이 아니며 대체 Reference로 쓰지 않는다(§8.1).
|
||||
- **Confirmed 표기 해석**: border 1px·padding 24px 등은 ui_viz에서 `Confirmed`가 아니라 owner 채택 Canonical 값이다. Confirmed만 적용한다는 기준으로 보면 재확인이 필요한 항목이다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/card.tsx
|
||||
packages/ui/src/theme/card.css
|
||||
packages/ui/src/theme/primitives.css (--hm-brown-light-4, --hm-warm-neutral-pale)
|
||||
packages/ui/src/theme/default.css (--hm-sem-stroke-input-default, --hm-sem-palette-brown-light-4 연결)
|
||||
packages/ui/src/theme/contract.ts (Runtime key color.stroke.input.default, color.palette.brown-light-4)
|
||||
themes/default.json (HM Default 컬러 원본)
|
||||
packages/ui/src/theme.css (card.css import)
|
||||
src/components/card.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
src/patterns/dashboard-layout.stories.tsx (CardDescription 소비 예시)
|
||||
src/App.tsx (Demo, hm-panel* class 사용)
|
||||
src/hm-theme.css (.hm-panel* Demo 전용 스타일, §8.1)
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(대표 Story 4개 구역): background White/Beige(`rgb(255,255,255)` / `rgb(245,240,229)`), border 1px `rgb(222,219,210)` / `rgb(236,212,162)`, radius 4px, shadow none, 위 padding 24px, Header·Content 좌우 24px, Header→Content 24px, overflow visible, Title 20px/28px/600 `rgb(10,34,29)`, column-gap 20px, Action 교차축 center, Action–Title 간격 20px — 문서 값과 일치.
|
||||
- Compatibility: Footer 아래 모서리 3px, `size="sm"` 위 padding 12px·Title 14px 확인.
|
||||
- Demo(App): 4개 Card가 border 1px/radius 4px/shadow none, 위 padding 16px, row-gap 0으로 렌더 확인. 이 값은 Demo `.hm-panel` 결과이며 운영 Card 값이 아니다(§8.1).
|
||||
- 2026-10-08 문서 정정: Border Token 연결(`card.css` ↔ `default.css` ↔ `contract.ts` ↔ `themes/default.json`), CardDescription 사용처, Demo class 규칙을 코드와 대조했다. `card.tsx`·`card.css`는 주석만 고쳤고 CSS 값·Component 동작·공개 API·Story는 바꾸지 않았다.
|
||||
- Story: `All` + verification 3개 ID 확인, 기본 sidebar는 `All` 하나만 보이는 1뎁스 구조(실제 사이드바 화면은 확인하지 못함).
|
||||
- 미확인: 사이드바 실제 화면, 브라우저별 렌더, Demo 화면 시각 비교(그림자 제거로 달라짐), 이미지 포함 Card.
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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` 표시), 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 동작.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Checkbox
|
||||
|
||||
현재 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는 변경하지 않음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Collapsible
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Collapsible 고유의 API·Token·상태·결정만 기록한다. Accordion(`accordion.md`)과 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Collapsible (Radix `Collapsible`) |
|
||||
| 대표 Story | `Components/Disclosure/Collapsible` → `All` (이름 "Collapsible") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Collapsible이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Collapsible 구조(Root·Trigger·Content)를 유지. shadcn Collapsible은 스타일이 없는 primitive라, 일반 trigger(asChild 없음)에만 텍스트 trigger 모양을 주고 content 간격·타이포를 정했다.
|
||||
- **Accordion과의 구분**: Collapsible은 단일 영역의 간단한 disclosure다. divider 구조와 chevron 내장이 없다. Accordion을 항목 하나로 쓰는 용도가 아니다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Trigger(일반) | inline-flex, gap 8px, 배경·border·padding 없음, 14 / 20 / 500, `--hm-sem-fg-primary`, 포인터 커서 |
|
||||
| Trigger(`asChild`) | 자식이 자기 스타일을 유지(예: HM Button). HM trigger 스타일은 적용되지 않는다 |
|
||||
| Indicator | trigger 바로 아래 자식 `svg`가 있으면 16px, `--hm-sem-fg-secondary`, 열리면 180° 회전 |
|
||||
| Content | 위 padding 8px, 14 / 20 / 400, `--hm-sem-fg-primary` |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere`, 고정 높이 없음 |
|
||||
| Divider / border | 없음 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`. 닫힌 content는 `hidden`(display: none) |
|
||||
| Hover | 일반 trigger 밑줄(disabled 제외) |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` (일반 trigger) |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed` |
|
||||
| Animation | 없음(shadcn Collapsible에 없고 HM Motion 규칙도 없다) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Collapsible props 그대로. export: `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent`.
|
||||
|
||||
- `Collapsible`: `open`/`defaultOpen`/`onOpenChange`, `disabled`.
|
||||
- `CollapsibleTrigger`: `asChild`. **`asChild`일 때는 `data-slot`을 붙이지 않는다**(DialogClose와 같은 규칙). 붙이면 자식 Button의 `data-slot="button"`이 덮여 HM Button 스타일이 사라진다. 일반 trigger 출력은 `data-slot="collapsible-trigger"`.
|
||||
- `CollapsibleContent`: 출력 `data-slot="collapsible-content"`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/collapsible.css` (`--hm-collapsible-*`).
|
||||
|
||||
`trigger-text`, `trigger-gap`, `trigger-font-size|line-height|font-weight`, `icon-size`, `icon-color`, `content-gap`, `content-text`, `content-font-size|line-height`, `disabled-opacity`. Semantic만 참조하고 `--hm-accordion-*` 등 다른 컴포넌트의 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`aria-expanded`, `aria-controls`)·Enter/Space·controlled/uncontrolled·disabled·`asChild`, `data-slot`, `cn(..., className)`, Content의 `hidden` 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- 일반 trigger의 텍스트 trigger 스타일, Content 간격·타이포, indicator 회전은 shadcn에 없던 시각 규격을 추가한 것이다.
|
||||
- `CollapsibleTrigger`의 `asChild` data-slot 처리(위 §5).
|
||||
- Animation은 추가하지 않았다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Collapsible"): `StateBoard`로 Closed, Open, Disabled, Long Content, Controlled + Button trigger를 한 페이지에 둔다. trigger 안 chevron은 일반 소비 조합(`lucide-react` 아이콘)이다.
|
||||
- verification: `Closed`, `Open`, `Disabled`, `Long Content`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(14/20/500, gap 8px, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 열림/닫힘 animation 없음. Motion 규칙이 정해지면 Accordion과 함께 재검토.
|
||||
- indicator는 컴포넌트에 내장하지 않고 소비 조합으로 둔다(Accordion과 다름). 내장 여부는 사용 사례가 쌓이면 결정.
|
||||
- 박스형(border/배경) Content variant는 만들지 않았다.
|
||||
- Hover 밑줄은 일반 trigger에만 적용한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/collapsible.tsx
|
||||
packages/ui/src/theme/collapsible.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/collapsible.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Enter로 열기(`aria-expanded=true`, indicator 180° 회전), Space로 닫기, 닫힌 content `display: none`, disabled 클릭 무반응(opacity 0.6, cursor not-allowed), controlled(Button trigger asChild)에서 클릭으로 열리고 상태 문구와 버튼 라벨(`접기`)이 갱신됨.
|
||||
- Button trigger: `data-slot="button"`, `data-variant="primary"`와 HM Button 색이 유지됨.
|
||||
- computed style: 일반 trigger 14/20/500, `rgb(10,34,29)`, 배경 투명, icon 16px, content 위 padding 8px. 긴 콘텐츠에서 가로 overflow 없음.
|
||||
- 미확인: 포인터 클릭(JS click과 키보드만 사용), focus ring 시각, 390px 폭, uncontrolled `defaultOpen=false`에서 trigger 클릭(키보드 Enter로 대체 확인).
|
||||
@@ -0,0 +1,132 @@
|
||||
# Combobox
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Combobox 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Reusable component, Popover + Command 조합** (typed `options` wrapper) |
|
||||
| 구현 상태 | 구현 완료 (single-select, 현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | `Popover`(public API) + `Command`(public API) 조합. shadcn 공식 Combobox는 예시 조합이다 |
|
||||
| 대표 Story | `Components/Search & Date/Combobox` → `All` (이름 "Combobox") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Combobox가 없다(Select와 Search Field는 다른 컴포넌트). 참고할 수치가 없었다.
|
||||
- **Component로 만든 이유**: 검색 가능한 단일 선택은 서비스에서 반복되고(trigger 텍스트, 검색, 선택 표시, 닫힘, focus 복귀가 동일) 최소 API(`options`)로 충분해서 typed wrapper를 제공한다. 더 복잡한 항목(그룹, 아이콘 등)은 소비자가 `Command`를 직접 조합한다.
|
||||
- **구현 방식**: Popover(Portal, collision, Escape, focus 복귀)와 Command(검색, 필터링, 키보드, listbox ARIA)를 그대로 조합한다. Select의 구현이나 연결형 윤곽을 복제하지 않았다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. Select와의 구분
|
||||
|
||||
| | Select | Combobox |
|
||||
|---|---|---|
|
||||
| 용도 | 정해진 짧은 목록에서 선택, 검색 불필요 | 검색/필터가 필요한 선택 |
|
||||
| 구현 | Radix Select | Popover + Command(cmdk) |
|
||||
| 목록 | trigger에 붙은 연결형 윤곽, 선택 항목은 면으로 표시 | 독립 floating 패널(검색 Input + 목록), 선택 항목은 Accent 45% 면 + check 아이콘 |
|
||||
| 접근성 모델 | Radix Select(listbox) | `role=combobox` trigger + 패널 안 cmdk combobox/listbox |
|
||||
|
||||
## 4. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `large`(기본, 44px, 16 / 24), `medium`(34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 chevron 18px |
|
||||
| 값 / placeholder | 선택된 option의 label, 없으면 placeholder("선택", `--hm-sem-fg-placeholder`), 길면 말줄임 |
|
||||
| Popup | padding 0, 폭 = trigger 폭(`--radix-popover-trigger-width`), 독립 4px gap. Focus Light 2 border, input radius 4px(네 모서리), shadow `0 2px 24px black 12%` |
|
||||
| 검색 | Command Input(placeholder 기본 "검색...") |
|
||||
| 목록 | Command List(최대 280px), Item 높이 36px·padding-x 12px·radius 0·weight 400. Large 16/24, Medium 14/20. 선택 check 16px, 긴 label은 말줄임 |
|
||||
| Empty | "결과가 없습니다."(`emptyText`) |
|
||||
|
||||
## 5. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Focus / Open | trigger 테두리 전체가 `--hm-sem-stroke-input-focus` (Input·Select와 같은 방식) |
|
||||
| Highlighted option | `--hm-sem-surface-highlight` (Accent 45%) |
|
||||
| Selected option | check 아이콘 + Accent 45% 면(`data-checked`). Command highlight가 다른 항목으로 옮겨져도 유지 |
|
||||
| Disabled (combobox) | opacity 0.6(이 컴포넌트 값), cursor not-allowed, 열리지 않음 |
|
||||
| Disabled option | Command의 disabled(opacity 0.6), 방향키 이동·선택 제외 |
|
||||
| 선택 후 | 값 갱신, Popover 닫힘, focus가 trigger로 복귀. 다시 열면 검색어가 비어 있다(Command가 닫을 때 unmount됨) |
|
||||
|
||||
## 6. API
|
||||
|
||||
export: `Combobox`, types `ComboboxOption`(`{ value, label, disabled? }`), `ComboboxSize`.
|
||||
|
||||
- `options`(필수), `value`/`defaultValue`/`onValueChange(value)`, `placeholder`, `searchPlaceholder`, `emptyText`, `size`, `disabled`, 그리고 `button` props(`id`, `aria-label`, `className`…).
|
||||
- 필터링: option의 `value`와 `label`(keywords)에 cmdk 기본 fuzzy 필터를 쓴다.
|
||||
- 이미 선택된 option을 다시 선택해도 값은 유지된다(해제 UI는 없다).
|
||||
- **접근성**: Trigger는 `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`. 패널 안 Command Input이 `role=combobox`(`aria-controls` → `role=listbox`), option은 `role=option`. 이름은 `<label htmlFor=id>` 또는 `aria-label`을 소비자가 준다. 실제 ARIA combobox(편집 가능한 trigger input)가 아니라 **버튼 trigger + 패널 내 검색 combobox** 구조다(shadcn 공식 구조).
|
||||
- generic data framework, 다중 선택, 비동기 로딩은 만들지 않았다.
|
||||
|
||||
### 값 소유권과 외부 초기화
|
||||
|
||||
- `controlled?: boolean`은 선택적 값 소유권 설정이다. `true`이면 `value`가 `undefined`여도 Controlled이며, 부모 값만 표시한다. `undefined`는 선택 없음이다. 사용자 선택은 기존 `(string) => void` 콜백으로 전달하며, 부모가 값을 반영하지 않으면 표시값은 유지된다.
|
||||
- `controlled={false}`이면 내부 state가 소유권을 가진다. `value`는 사용하지 않고 `defaultValue`를 최초 마운트 시에만 적용한다. 이후 `defaultValue` 변경은 초기화가 아니다. 사용자 선택은 내부 state를 갱신하고 콜백을 호출한다.
|
||||
- **Legacy 호환**: `controlled`를 생략하면 기존 `value !== undefined` 판정을 유지한다. 문자열이 있으면 Controlled, `undefined`이면 Uncontrolled이다. 이 방식으로 외부 값을 `undefined`로 바꾸면 과거 내부 값/defaultValue가 다시 표시될 수 있다. 기존 소비 코드의 동작은 변경하지 않았다.
|
||||
- 신규 Controlled 사용과 FilterBar 연동은 `controlled={true}`를 사용한다. `controlled` 옵션과 값 소유권 모드는 마운트 중 변경하지 않는 사용 계약을 권장한다. Controlled에서 `defaultValue`를 함께 전달해도 부모 `value`가 우선한다.
|
||||
- `""`은 유효한 문자열 옵션 값이며 `undefined`와 동일한 빈 값으로 정규화하지 않는다. `value=""`는 빈 문자열 옵션이 있으면 그 label을 표시하고, 없으면 기존처럼 placeholder를 표시한다. 빈 문자열을 가진 항목을 선택하면 콜백은 `""`을 전달한다.
|
||||
|
||||
```tsx
|
||||
const [category, setCategory] = useState<string | undefined>()
|
||||
|
||||
<Combobox controlled={true} options={categories} value={category} onValueChange={setCategory} />
|
||||
// 외부 항목 지정/갱신: setCategory("design")
|
||||
// 외부 초기화: setCategory(undefined) — key remount 불필요
|
||||
```
|
||||
|
||||
외부 값 갱신/초기화는 `onValueChange`를 호출하지 않는다. FilterBar의 부모는 항목 state와 조회 조건을 함께 초기화한다. 선택값 clear는 팝업 닫기나 검색어 초기화를 의미하지 않는다. 열린 팝업의 검색어를 강제로 지우는 새 계약이나 clear UI는 추가하지 않는다. `controlled`를 생략한 Legacy 호출의 remount 우회는 이 신규 계약과 구분한다.
|
||||
|
||||
## 7. Dependency
|
||||
|
||||
새 dependency 없음(`Command`의 `cmdk`, `Popover`의 Radix를 그대로 사용).
|
||||
|
||||
## 8. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/combobox.css` (`--hm-combobox-*`). Trigger와 선택 check만 스타일하며 Popover와 Command의 스타일은 중복 정의하지 않는다.
|
||||
|
||||
`height-md|lg`, `border-width`, `padding-x`, `gap`, `radius`, `font-size|line-height-md|lg`, `background`, `text`, `placeholder`, `border-color`, `focus-border-color`, `icon-size|color`, `check-size`, `disabled-opacity`. Semantic/Theme palette를 참조하며 `--hm-popover-*`, `--hm-command-*`, `--hm-select-*`는 참조하지 않는다. 기본 모양은 `@layer components`(소비자의 `className`이 이김). Trigger에는 `min-width: 0; max-width: 100%`를 두어 긴 값이 grid/flex 부모를 넓히지 않고 말줄임된다.
|
||||
|
||||
**Popover 결합 지점**: Popup의 padding과 폭을 `PopoverContent`의 inline style로 `--hm-popover-padding: 0px`, `--hm-popover-width: var(--radix-popover-trigger-width)`로 지정해 Popover token 값을 인스턴스 단위로 덮는다(Popover 코드는 변경하지 않음).
|
||||
|
||||
## 9. 조합한 컴포넌트에서 유지한 항목
|
||||
|
||||
Popover: Portal, collision, Escape, 바깥 클릭 닫힘, trigger focus 복귀. Command: 필터링, 방향키/Home/End/Enter, disabled 건너뜀, Empty, listbox/option ARIA.
|
||||
|
||||
## 10. Storybook
|
||||
|
||||
- 대표 Story `All`("Combobox"): Empty placeholder + search(Remix는 disabled option), Selected(controlled, 값 표시), Medium, Long option, Disabled를 한 페이지에 둔다. Label은 Story 조합이다.
|
||||
- verification: `Empty`, `Controlled`, `Long Option`.
|
||||
|
||||
## 11. 사용자 확정 결정 / 보류
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다. Combobox는 최소 `options` wrapper로 제공한다(작업 지시 B안).
|
||||
- 보류: 수치(44/34px, check 아이콘 위치)는 현재 HM 규격이며 디자인 확정 시 재검토. clear 버튼, 다중 선택(chips), 그룹 option, 새 항목 생성, 비동기 검색, 편집 가능한 trigger(input형 combobox)는 만들지 않았다. 선택 option의 label 줄바꿈 표시(긴 옵션 전체 보기)는 말줄임으로 대체.
|
||||
- Popup 방향/collision은 Popover 기본(`align="start"`).
|
||||
- 2026-10-06 Visual QA: `combobox.css`의 `[data-combobox-popup]` scope로 Select의 시각 언어와 정합. Command/Popover 공용 CSS·동작은 유지. 대표 Story의 Select 비교 구역에서 두 size를 확인한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/combobox.tsx
|
||||
packages/ui/src/theme/combobox.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/combobox.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 Controlled 초기화: 실제 소스를 직접 묶은 격리 Headless Edge 환경에서 수정 전 과거 값 재표시를 재현했다. `controlled={true}`의 undefined → A → B → undefined, 최초 Controlled 값 clear, 부모 선택 거부, 외부 값 갱신, 콜백 횟수, Legacy/defaultValue/명시적 Uncontrolled, 빈 문자열 옵션, 열린 팝업 clear와 검색어/focus 유지, 키보드 선택·disabled 제외·focus 복귀를 확인했다. 소스 기준 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 popup 폭=trigger, gap 4px, 선택 면 지속, 검색/Empty/키보드 선택·focus 복귀·disabled 건너뜀·긴 label 말줄임 통과. Select 비교에서 Item 높이는 36/42px를 유지하고 Medium 14/20·Large 16/24 typography를 일치시켰다. Command/Popover 공용 변경 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): trigger에서 Enter로 열림(`role=combobox`, `aria-expanded` true, Popup 폭 = trigger 폭 320px), 검색 Input으로 focus 이동, 첫 항목 highlight, 검색 "nu" → Nuxt만 남아 highlight, Enter로 선택하면 trigger가 "Nuxt"로 바뀌고 Popover가 닫히며 focus가 trigger로 복귀. ArrowDown 이동에서 disabled `Remix` 건너뜀(Nuxt → Astro), Escape로 닫힘과 trigger focus 복귀(값 유지), 결과 없음 "zzz"는 Empty 문구, 다시 열면 검색어가 비어 있고 전체 5개 표시.
|
||||
- 선택 표시: 선택된 option에만 check 아이콘(opacity 1)과 `data-checked`. 클릭으로 선택하면 trigger와 controlled 값이 갱신됨.
|
||||
- Popover 스타일 확인: border `rgb(222,219,210)`, radius 4px, 그림자, padding 0, 흰 면(처음에 `data-slot` 덮어쓰기로 이 스타일이 빠진 문제를 수정함).
|
||||
- Responsive(390×844): trigger 320px, popup(304px)이 viewport 안에 들어감, 긴 option의 값과 항목이 말줄임, 페이지 가로 overflow 없음(처음에는 trigger의 `min-width:auto` 때문에 페이지가 833px로 밀리는 문제가 있어 `min-width:0`으로 수정).
|
||||
- 미확인: 클릭 선택 직후 닫힘 animation 시각, 포인터 hover 시각, 화면 하단에서 popup이 위로 flip 되는 경우, 스크린리더 실제 읽기.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Command
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Command 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Primitive component** (검색 가능한 command/list). Combobox 자체가 아니다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Command = **cmdk** |
|
||||
| 대표 Story | `Components/Search & Date/Command` → `All` (이름 "Command") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Command가 없다(Search Field의 후보 목록은 다른 컴포넌트). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Input·Popover·Dropdown Menu Token은 참조하지 않는다(Command Input은 Command 전용 Token).
|
||||
- **구현 방식**: cmdk가 검색 필터링, 방향키/Home/End 이동, Enter 선택, disabled 건너뜀, 그룹, Empty, listbox/option ARIA를 소유한다. HM은 cmdk의 속성(`data-selected`, `data-disabled`, `[cmdk-group-heading]`)에 시각을 연결한다.
|
||||
- **범위**: 전역 Command Palette(`CommandDialog`)는 만들지 않았다(별도 고급 패턴).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Command | 흰 면(`--hm-sem-bg-surface`), 글자 `--hm-sem-fg-primary`, radius 4px. **border·shadow 없음**(Popover 등 컨테이너 안에서 쓰는 것을 전제) |
|
||||
| Input | 높이 40px, padding-x 12px, 14 / 20, 아래 1px `--hm-sem-stroke-input-default` 구분선, 검색 아이콘 16px `--hm-sem-fg-secondary`, placeholder `--hm-sem-fg-placeholder` |
|
||||
| List | 최대 높이 280px(넘치면 세로 scroll), padding 4px |
|
||||
| Item | 최소 높이 36px, padding-x 12px, gap 8px, radius 2px, 14 / 20, 아이콘 16px |
|
||||
| Group heading | 12 / 18 / 500, `--hm-sem-fg-secondary`, padding `6px 12px` |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, List 폭 전체 |
|
||||
| Shortcut | 12px, `--hm-sem-fg-secondary`, letter-spacing .05em |
|
||||
| Empty | padding 24px 12px, 가운데, `--hm-sem-fg-secondary` |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Highlighted (`data-selected="true"`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 글자 `--accent-foreground`. 포인터 hover와 키보드 이동이 공유 |
|
||||
| Disabled (`data-disabled="true"`) | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 방향키 이동에서 제외 |
|
||||
| Empty | 필터 결과가 없을 때 `CommandEmpty` 표시 |
|
||||
| Focus | Input에 있음(`outline-hidden`). 이동은 Highlighted 면으로 표시 |
|
||||
|
||||
## 5. API
|
||||
|
||||
cmdk props 그대로. export: `Command`, `CommandInput`, `CommandList`, `CommandEmpty`, `CommandGroup`, `CommandItem`, `CommandSeparator`, `CommandShortcut`.
|
||||
|
||||
- `Command`: `value`/`onValueChange`(highlight), `filter`(사용자 정의 필터), `shouldFilter`, `loop`, `label`.
|
||||
- `CommandItem`: `value`, `keywords`, `disabled`, `onSelect`.
|
||||
- `CommandInput`: Input + 검색 아이콘 wrapper(`data-slot="command-input-wrapper"`)를 포함한다.
|
||||
- **필터링은 cmdk 기본(fuzzy) 그대로**이며 HM이 검색 알고리즘을 만들지 않았다.
|
||||
- 접근성: `CommandInput`은 `role="combobox"`, `aria-expanded`, `aria-controls`(listbox), `aria-autocomplete="list"`; `CommandList`는 `role="listbox"`; `CommandItem`은 `role="option"`.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `cmdk` ^1.1.1 (`packages/ui`에 추가, shadcn Command의 기반). CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/command.css` (`--hm-command-*`).
|
||||
|
||||
`background`, `text`, `radius`, `input-*`(height, padding-x, gap, font, placeholder, icon), `divider-color`, `list-max-height|padding`, `item-*`(height, padding-x, gap, radius, font), `highlight-background|text`, `group-label-*`, `shortcut-*`, `empty-*`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`)만 참조한다. 신규 Primitive와 Semantic 없음. 이 CSS는 cmdk 내부 속성에 맞춘 일반 CSS이며 `@layer`를 쓰지 않는다(`className`으로 Command의 높이 등을 바꾸는 것은 TSX class 영역).
|
||||
|
||||
## 8. cmdk에서 유지한 항목
|
||||
|
||||
필터링과 정렬, 키보드(ArrowUp/Down, Home/End, Enter, disabled 건너뜀), 그룹 숨김(결과 없는 그룹), Empty, `data-selected`/`data-disabled`, listbox/option ARIA, 마우스 hover가 highlight를 옮기는 동작.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Command"): Search + disabled item(선택 로그), Groups·icons·shortcut(`CalendarIcon`, `UserIcon`, `SettingsIcon`), Long list(30개, List가 scroll)를 한 페이지에 둔다. 테두리 박스는 Story 조합이다.
|
||||
- verification: `Search`, `Grouped`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(40px Input, 36px Item, 280px List 최대 높이)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Command Palette(`CommandDialog`, 전역 단축키 호출)는 별도 패턴으로 남겼다.
|
||||
- 로딩 상태(`Command.Loading`), 중첩 페이지는 스타일링하지 않았다.
|
||||
- 선택됨(value) 표시는 Command에 없다(highlight와 다르다). Combobox가 check 아이콘으로 따로 표시한다.
|
||||
- Command의 Escape는 Command 자체에서는 하는 일이 없다(Popover/Dialog 안에서는 컨테이너가 닫는다).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/command.tsx
|
||||
packages/ui/src/theme/command.css
|
||||
packages/ui/package.json (dependency: cmdk)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/command.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- ARIA(Browser pane): Input `role=combobox`·`aria-expanded=true`·`aria-controls`·`aria-autocomplete=list`, List `role=listbox`, Item `role=option`, disabled Item에 `data-disabled="true"`.
|
||||
- keyboard: 첫 항목이 처음부터 highlight, ArrowDown으로 이동(비활성 항목 건너뜀: 바나나 → 포도), Enter로 선택(`onSelect` 로그 갱신), 그룹 간 이동(ArrowUp 환경설정 → 프로필), End로 마지막 활성 항목(비활성 `결제`는 제외).
|
||||
- 검색: 한글 "바나" 입력 시 바나나만 표시, 결과 없는 "zzz"는 Empty 문구, 입력을 지우면 전체 4개로 복귀하고 첫 항목이 highlight. Escape는 Input focus를 유지(Command 단독에서는 동작 없음).
|
||||
- computed style: Input 래퍼 40px, List 최대 280px·padding 4px, Item 36px·padding `0 12px`·14/20, Highlighted `rgb(189,223,212)`, Group heading 12px/500 `rgb(110,103,89)`, Shortcut 12px.
|
||||
- 미확인: 긴 List의 키보드 scroll 시각, 모바일 폭(Combobox 안에서만 확인), pointer hover 시각, `CommandItem`의 `keywords` 외 사용자 정의 `filter`.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Context Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Context Menu 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu와 같은 메뉴 계열이므로 상세 값은 `dropdown-menu.md`와 대응한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Context Menu (Radix `ContextMenu`, `radix-ui` 1.6.7 / `@radix-ui/react-context-menu` 2.3.7) |
|
||||
| 대표 Story | `Components/Overlay/Context Menu` → `All` (이름 "Context Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 Context Menu 규격이 없다. 최종 기준은 현재 `@hm-design/ui` 구현과 Dropdown Menu 문서다.
|
||||
- **현재 HM 규격**: 면·윤곽·radius·그림자는 Floating Surface Semantic(`--hm-sem-floating-*`)을 source로 쓰고, item·text 값은 Dropdown Menu와 같은 현재 HM 값이다.
|
||||
- **구현 방식**: shadcn / Radix Context Menu 구조 유지. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
|
||||
## 3. Dropdown Menu와 역할 차이
|
||||
|
||||
| | Dropdown Menu | Context Menu |
|
||||
|---|---|---|
|
||||
| 여는 방법 | 명시적 버튼(Trigger) 클릭 / Enter / Space / 방향키 | 대상 영역 우클릭, context-menu key / Shift+F10, 터치 long press |
|
||||
| 위치 | Trigger 기준 anchored (`sideOffset` 4) | 포인터 위치 기준 (`sideOffset` 없음) |
|
||||
| 용도 | 버튼이 가진 Action 목록 | 대상 객체에 대한 보조 Action |
|
||||
| 닫힐 때 focus | Trigger로 복귀 | Trigger가 포커스 가능한 요소일 때만 복귀(§7) |
|
||||
|
||||
브라우저 native context menu 대신 HM Context Menu가 열린다(Radix가 `contextmenu` 이벤트를 막음).
|
||||
|
||||
## 4. 기본 규격 / 상태 규격
|
||||
|
||||
`dropdown-menu.md` §3·§4와 값이 같다(Surface White, 1px pale border, radius 4px, 12px glow 그림자, item 36px·14/20, Highlighted Accent 45%(`--hm-sem-surface-highlight`), Disabled opacity 50, Destructive `--hm-sem-action-destructive`, Check 16px / Radio 8px). 값이 같아도 `--hm-dropdown-menu-*`는 참조하지 않는다.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Context Menu props 그대로. export: `ContextMenu`, `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuGroup`, `ContextMenuLabel`, `ContextMenuItem`, `ContextMenuCheckboxItem`, `ContextMenuRadioGroup`, `ContextMenuRadioItem`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuSub`, `ContextMenuSubTrigger`, `ContextMenuSubContent`, `ContextMenuPortal`.
|
||||
|
||||
- `ContextMenuItem`: shadcn `variant="default" | "destructive"`, `inset`. Label·CheckboxItem·RadioItem·SubTrigger: `inset`.
|
||||
- 출력 속성: `data-slot="context-menu-*"`, Radix `data-state`·`data-highlighted`·`data-disabled`·`data-side`.
|
||||
- HM 전용 prop 없음.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/context-menu.css` (`--hm-context-menu-*`).
|
||||
|
||||
- Surface 5개(`background`, `border-width`, `border-color`, `radius`, `shadow`)는 **Floating Surface Semantic을 source로 참조**한다: `--hm-sem-floating-background|border-width|border-color|radius|shadow`.
|
||||
- 나머지(`text`, `padding`, `item-*`, `font-*`, `highlight-*`, `destructive-*`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`)는 Dropdown Menu와 같은 현재 HM 값을 자체 정의한다.
|
||||
- 다른 컴포넌트의 Component Token을 쓰지 않는다. 신규 Primitive·Semantic 없음. 기존 Floating Semantic을 유지한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menu`, `menuitem*`)·키보드(방향키, Home/End, Enter/Space, 오른쪽 방향키 sub menu, Escape, typeahead)·Portal·collision·`data-slot`·`cn(..., className)`·open/close animation class·`z-50`·lucide 아이콘.
|
||||
|
||||
**접근성 주의**: Radix `ContextMenuTrigger`는 기본적으로 포커스할 수 없는 `span`이다. 키보드 사용자가 여는 경로(Shift+F10 / context-menu key)를 제공하려면 소비자가 Trigger 영역에 `tabIndex={0}`을 주거나 `asChild`로 포커스 가능한 요소(버튼, 행 등)를 감싸야 한다. 우클릭만으로 접근 가능한 기능을 만들지 않는다. 대표 Story의 영역은 `tabIndex={0}`을 준다.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
Dropdown Menu와 동일: shadcn 시각 class(`rounded`, `shadow`, `p-1`, `focus:bg-accent` 등)를 제거하고 theme CSS + `data-highlighted`로 대체. Radio는 CSS 크기 token을 쓰는 원(`context-menu-radio-dot`), Check / Radio indicator span에 `data-slot="context-menu-item-indicator"` 추가. Context Menu에는 `sideOffset`이 없다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Context Menu"): 점선 영역을 우클릭하면 전체 메뉴가 열린다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive).
|
||||
- verification: `Open`(로드 시 합성 `contextmenu` 이벤트로 열린 상태, fullscreen).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: Context Menu는 Dropdown Menu와 같은 floating menu 계열이므로 Floating Surface Semantic을 Component Token의 source로 쓴다. Component Token은 유지하고 `--hm-dropdown-menu-*`는 참조하지 않는다(작업 지시).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치는 Dropdown Menu와 같은 현재 HM 규격이며 디자인 확정 시 함께 재검토.
|
||||
- Home/End·typeahead·long press(터치)는 확인하지 못했다.
|
||||
- Shift+F10 / context-menu key는 자동화 입력이 브라우저의 native `contextmenu` 이벤트를 만들지 못해 확인하지 못했다(Radix는 이 이벤트로 여는 구조이며 우클릭과 같은 경로).
|
||||
- 메뉴가 닫힌 뒤 Trigger가 포커스 불가면 focus는 body에 남는다(Radix 동작).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/context-menu.tsx
|
||||
packages/ui/src/theme/context-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/context-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 interaction(Browser pane): 우클릭 시 native 메뉴 대신 HM 메뉴가 열림, 방향키 이동(disabled 항목 건너뜀), Enter로 Checkbox 토글(메뉴 유지), 방향키 아래 + 오른쪽으로 Sub menu 열림과 첫 항목 focus, Escape 두 번으로 sub → menu 순서 닫힘.
|
||||
- computed style: Content·Sub Content 모두 white, `1px solid rgb(222,219,210)`, radius 4px, `0 0 12px .2` 그림자, item 36px, Dropdown Menu(`Open` Story) 값과 동일. role `menu`.
|
||||
- Dropdown Menu의 같은 값이 변하지 않음을 확인(회귀 없음).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Date Picker
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Date Picker 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Reusable component, Popover + Calendar 조합** (새 날짜 engine 없음) |
|
||||
| 구현 상태 | 구현 완료 (single-date, 현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | `Popover`(public API) + `Calendar`(public API) 조합. shadcn 공식 Date Picker는 예시 조합이다 |
|
||||
| 대표 Story | `Components/Search & Date/Date Picker` → `All` (이름 "Date Picker") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Date Picker가 이름뿐이고 규격이 없다. 참고할 수치가 없었다.
|
||||
- **Component로 만든 이유**: 서비스의 모든 날짜 선택이 같은 trigger 텍스트 형식, 선택 후 닫힘, 값 처리, focus 복귀를 필요로 해 반복 사용이 확실하다고 판단했다. 값 처리(`value`/`defaultValue`/`onValueChange`)는 패키지가 소유한다.
|
||||
- **구현 방식**: Popover(Portal, collision, Escape, focus 복귀)와 Calendar(날짜 모델, 키보드)를 그대로 조합한다. Popover 내부 구현을 복제하지 않았다.
|
||||
- **Trigger**: Button이 아니라 Input/Select의 field language를 따르는 필드형 버튼이다. Button/Input/Select Token은 참조하지 않고 `--hm-date-picker-*`로 관리한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `large`(기본, 안쪽 42px + 1px border = 44px, 16 / 24), `medium`(안쪽 32px = 34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 달력 아이콘 18px `--hm-sem-fg-secondary` |
|
||||
| 값 / placeholder | 선택값은 `--hm-sem-fg-primary`, placeholder("날짜 선택")는 `--hm-sem-fg-placeholder`, 길면 말줄임 |
|
||||
| 날짜 표시 | 로컬 `Date`의 연·월·일 → `YYYY.MM.DD` (예: `2026.01.09`, `2026.10.06`). `format` prop으로 교체 |
|
||||
| Popup | Popover 면(흰 면, 1px pale border, 4px radius, 그림자)에 Popover padding 0, 폭 `auto`로 Calendar에 맞춤 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Empty / Selected | `data-empty`가 있으면 placeholder 색 |
|
||||
| Focus / Open | 테두리 전체가 `--hm-sem-stroke-input-focus`(ring 없음, Input·Select와 같은 방식). 열림은 Radix `data-state="open"` |
|
||||
| Invalid (`aria-invalid="true"`) | Trigger border 1px `--hm-sem-action-destructive`. 새 `invalid` / `error` / `status` prop 없음 |
|
||||
| Invalid + Keyboard Focus (`:focus-visible`) | Destructive border 유지 + Focus Semantic 3px / 50% outer ring |
|
||||
| Invalid + Open | Trigger Error border 유지. Calendar Selected는 기존 Active, 날짜 Keyboard Focus는 기존 Focus Semantic 유지 |
|
||||
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 열리지 않음 |
|
||||
| 선택 후 | 값 갱신, Popover 닫힘, focus가 trigger로 복귀 |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `DatePicker`, type `DatePickerSize`.
|
||||
|
||||
- `value`/`defaultValue`(Date), `onValueChange(date)`, `placeholder`, `size` `"medium" | "large"`(기본 `large`), `disabled`, `format(date) => string`, `calendarProps`(`disabled`, `startMonth`, `endMonth`, `locale`, `captionLayout`, `weekStartsOn`, `today`), 그리고 `button` props(`id`, `aria-label`, `className`…).
|
||||
- 선택은 `required`라 선택된 날짜를 다시 눌러도 해제되지 않는다(해제/clear UI는 없음).
|
||||
- **접근성**: Trigger는 `button`에 Radix Popover의 `aria-haspopup="dialog"`, `aria-expanded`, 팝업은 `role="dialog"`. 접근 가능한 이름은 `<label htmlFor=id>`(Story) 또는 `aria-label`을 소비자가 준다. 선택값은 버튼 텍스트로 읽힌다.
|
||||
- Popover가 열릴 때 Radix 기본은 첫 tabbable(이전 달 버튼)에 focus하므로, DatePicker가 `onOpenAutoFocus`로 **Calendar의 tab stop(선택일, 없으면 오늘)에 focus**를 보낸다.
|
||||
- 범위(range) Date Picker는 만들지 않았다(필요가 명확해질 때 추가).
|
||||
|
||||
### 값 소유권과 외부 초기화
|
||||
|
||||
- `controlled?: boolean`은 선택적 값 소유권 설정이다. `true`이면 `value`가 `undefined`여도 Controlled이며, 부모 값만 표시한다. `defaultValue`는 표시값을 대신하지 않는다. 사용자 선택은 기존 `(Date | undefined) => void` 콜백으로 전달하며, 부모가 값을 반영하지 않으면 표시값은 유지된다.
|
||||
- `controlled={false}`이면 내부 state가 소유권을 가진다. `value`는 사용하지 않고 `defaultValue`를 최초 마운트 시에만 적용한다. 이후 `defaultValue` 변경은 초기화가 아니다. 사용자 선택은 내부 state를 갱신하고 콜백을 호출한다.
|
||||
- **Legacy 호환**: `controlled`를 생략하면 기존 `value !== undefined` 판정을 유지한다. 날짜가 있으면 Controlled, `undefined`이면 Uncontrolled이다. 이 방식으로 외부 값을 `undefined`로 바꾸면 과거 내부 값/defaultValue가 다시 표시될 수 있다. 기존 소비 코드의 동작은 변경하지 않았다.
|
||||
- 신규 Controlled 사용과 FilterBar 연동은 `controlled={true}`를 사용한다. `controlled` 옵션과 값 소유권 모드는 마운트 중 변경하지 않는 사용 계약을 권장한다. Controlled에서 `defaultValue`를 함께 전달해도 부모 `value`가 우선한다.
|
||||
|
||||
```tsx
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
|
||||
<DatePicker controlled={true} value={date} onValueChange={setDate} />
|
||||
// 외부 날짜 지정/갱신: setDate(new Date(2026, 9, 6))
|
||||
// 외부 초기화: setDate(undefined) — key remount 불필요
|
||||
```
|
||||
|
||||
외부 값 갱신/초기화는 `onValueChange`를 호출하지 않는다. FilterBar의 부모는 날짜 state와 조회 조건을 함께 초기화한다. 선택값 clear는 팝업 닫기나 달력의 현재 월/focus 초기화를 의미하지 않으며, 새 clear UI를 추가하지 않는다. `controlled`를 생략한 Legacy 호출의 remount 우회는 이 신규 계약과 구분한다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
새 dependency 없음(`Calendar`의 `react-day-picker`, `Popover`의 Radix를 그대로 사용). 날짜 표시는 `Intl.DateTimeFormat`, 값은 native `Date`이며 별도 date 라이브러리를 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/date-picker.css` (`--hm-date-picker-*`). Trigger만 스타일하며 Popover와 Calendar의 스타일은 중복 정의하지 않는다. 선택일 색은 Calendar Selected(`--hm-sem-state-active`)를 그대로 상속하며 Date Picker 전용 selected 색이나 scope override는 없다.
|
||||
|
||||
`height-md|lg`, `border-width`, `padding-x`, `gap`, `radius`, `font-size|line-height-md|lg`, `background`, `text`, `placeholder`, `border-color`, `focus-border-color`, `icon-size|color`, `disabled-opacity`. Semantic/Theme palette를 참조하며 `--hm-popover-*`와 `--hm-calendar-*`는 참조하지 않는다. 기본 모양은 `@layer components`에 두어 소비자의 `className`(폭, 높이)이 이긴다.
|
||||
|
||||
**Popover 결합 지점**: Popup의 padding과 폭을 `PopoverContent`의 inline style로 `--hm-popover-padding: 0px`, `--hm-popover-width: auto`를 지정해 Popover의 token 값을 인스턴스 단위로 덮는다. Popover 코드를 바꾸지 않는 선택이며, Popover token 이름에 대한 약한 결합이다.
|
||||
|
||||
## 8. 조합한 컴포넌트에서 유지한 항목
|
||||
|
||||
Popover: Portal, collision 처리, Escape, 바깥 클릭 닫힘, trigger focus 복귀, `role=dialog`. Calendar: 키보드(방향키, Enter/Space, PageUp/Down), disabled 날짜, 월 이동, locale(기본 한국어).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Date Picker"): Empty, Selected(`2026.10.06`), 기본 형식(`2026.01.09`)/custom override, Controlled(값 표시), Medium, Disabled dates(5일 이전·일요일), Disabled를 한 페이지에 둔다. Label은 Story 조합이며 `today`는 Calendar prop으로 2026-10-02에 고정.
|
||||
- verification: `Empty`, `Selected`, `Controlled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다. Date Picker는 실제 소비 API가 필요한 Component로 만든다(작업 지시).
|
||||
- 2026-10-06 Visual QA: Trigger 기본 formatter만 `YYYY.MM.DD`로 변경. Calendar 날짜 셀·caption·locale, custom `format`은 유지.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 기본 표시 형식은 `YYYY.MM.DD`로 확정했다. 다른 형식은 `format`으로 교체한다.
|
||||
- 선택 해제(clear) 버튼, 직접 입력(typing) 입력, Range Date Picker, 시간 선택은 만들지 않았다.
|
||||
- 타임존: native `Date`의 로컬 시간 기준이다.
|
||||
- Popup의 방향/collision은 Popover 기본(`align="start"`, 아래쪽 우선).
|
||||
- 좁은 화면 전용 picker(바텀시트 등)는 만들지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/date-picker.tsx
|
||||
packages/ui/src/theme/date-picker.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/date-picker.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 Controlled 초기화: 실제 소스를 직접 묶은 격리 Headless Edge 환경에서 수정 전 과거 값 재표시를 재현했다. `controlled={true}`의 undefined → A → B → undefined, 최초 Controlled 값 clear, 부모 선택 거부, 외부 값 갱신, 콜백 횟수, Legacy/defaultValue/명시적 Uncontrolled, 열린 팝업 clear, 키보드 날짜 선택·focus 복귀를 확인했다. 소스 기준 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 `2026.10.06`, `2026.01.09`, custom override, Korean caption, 키보드 날짜 선택과 focus 복귀 통과. `typecheck`, `build`, `build-storybook` 통과. 아래 2026-10-02 interaction의 긴 한국어 날짜 표시는 당시 기록이다.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): trigger에서 Enter로 열림(`aria-haspopup=dialog`, `aria-expanded` true), 선택된 날짜/오늘에 focus, ArrowRight 2 → 3, Enter로 선택하면 trigger 텍스트가 `2026년 10월 3일`로 바뀌고 Popover가 닫히며 focus가 trigger로 복귀(controlled 값 `2026. 10. 3.`). 다시 열면 선택일(3)에 focus, Escape로 닫혀도 값은 유지되고 trigger로 복귀, 날짜 클릭(20)으로도 선택과 닫힘. Disabled trigger는 클릭해도 열리지 않음.
|
||||
- computed style: Large 44px / 16·24, Medium 34px / 14, border `rgb(222,219,210)`, radius 4px, placeholder `rgb(153,153,153)`, Disabled opacity 0.6·cursor not-allowed.
|
||||
- Responsive(390×844): trigger 폭 320px, popup 264px(왼쪽 23px)이 viewport 안에 들어가고 페이지 가로 overflow 없음.
|
||||
- 수정한 문제: 처음에 `PopoverContent`에 `data-slot="date-picker-content"`를 넘겨 Popover의 `data-slot`을 덮어쓰는 바람에 Popover 스타일(윤곽, 그림자)이 빠졌다. 제거했다.
|
||||
- 미확인: Disabled dates가 popup 안에서 선택 불가한 것(Calendar 문서의 검증으로 갈음), 포인터 hover 시각, 좁은 viewport의 collision flip, `calendarProps.locale` 교체.
|
||||
|
||||
- 숨김 `InvalidFocus` Story (`tags: ["verification"]`)를 추가하여 기본 Sidebar는 유지한다.
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 숨김 `InvalidFocus`의 실제 Trigger DOM `aria-invalid="true"` 확인. Trigger Invalid border computed 1px, Focus 전후 shadow none → 3px Focus ring. 대표 `All` Default / Selected / Medium / Disabled 유지.
|
||||
- Enter / Space open, ArrowRight 날짜 이동, Enter 선택 후 닫힘 및 Trigger focus return, Escape close / focus return, Tab / Shift+Tab 확인. 2026.10.06 → 2026.10.07 선택값 표시 확인.
|
||||
- Calendar Selected `rgb(0, 127, 115)`(Active), 선택 날짜 Focus `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 유지. 일반 Calendar 대표 Story의 Single 및 Range 끝점은 Active, Range 중간 면은 Accent 45% 유지. Calendar CSS/API 수정 없음.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Dialog
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Dialog 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
> 근거 문서: ui_viz `docs/components/dialog.md`(Dialog / Modal). 문서가 스스로 `NO_SOURCE_EVIDENCE`(Source Dashboard에 Dialog specimen 없음)라고 밝히며, 값의 상당수는 D-093(2026-09-29) Dialog-local Confirmed다. Foundation `elevation.*`, `z-index.*`, `motion.*`는 TBD이고 Dialog 범위 값을 Foundation token으로 올리지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`<DialogContent />`만으로 적용, `hm-dialog` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Dialog + Radix `Dialog` |
|
||||
| 대표 Story | `Components/Overlay/Dialog` → `All` (이름 "Dialog") |
|
||||
| 보류 | Overlay 수치, Close 버튼 규격, 초기 focus 대상, 좁은 화면 Actions 배치 등 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed (Dialog-local)**: radius 8px(USER_DECISION 2026-09-17), shadow `0 0 12px rgba(0,0,0,.2)`(D-093 D-088), 내용 기준 크기와 최대 너비·높이 공식(D-088), Header `24/24/16`·Title↔Description 8·Body 24·Actions `0/24/24`·Action gap 8(D-093), Header 아래 inset divider 위치·두께와 Footer divider 없음, Body만 scroll, 긴 텍스트 줄바꿈, 표는 Body 안 가로 scroll, 표면 `color.bg.surface`·전경 `color.fg.primary`, Title/Description/Body 타이포.
|
||||
- **CANONICAL_OVERRIDE**: Title `heading-md` 20/28 중 weight **700**은 Dialog Title에만 적용(D-006, 600 대체). Foundation `heading-md`는 600 그대로.
|
||||
- **현재 HM Actions**: Footer padding은 `16px 24px 24px`이며 divider는 없다. 위 ui_viz 도입 근거의 `0/24/24`와 구분한다.
|
||||
- **Temporary / TBD(적용했으나 확정값 아님)**: 외곽선·Header divider 색 `#DEDBD2`는 `Temporary`, Token relation TBD(DIALOG-10 FOLLOW_UP). 이전 확정 색(`#9A9284`/`#E0DACE`)은 대체됐고 D-093 실행 화면과 같은 값이라 현재 구현을 따랐다. 색 역할이 정해지기 전까지 Dialog 범위 값이다.
|
||||
- **PARTIAL / REFERENCE_ONLY(적용)**: Description 전경 `color.fg.secondary`(DIALOG-06 PARTIAL, 현재 style 보존). edge 16px(REVIEW).
|
||||
- **REFERENCE_ONLY(미적용)**: Backdrop Black 42%. 확정값이 아니어서 shadcn 기본 Overlay를 유지했다.
|
||||
- **HM 사용자 결정**: 없음.
|
||||
- **shadcn 유지**: Overlay, 열림/닫힘 animation, Close 버튼, Footer `showCloseButton`, `showCloseButton` prop.
|
||||
- **Radix 유지**: 아래 §7.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Overlay | shadcn 기본(`bg-black/10`, `backdrop-blur-xs`, `fixed inset-0`, z-50) | ui_viz backdrop 수치 TBD/REFERENCE_ONLY |
|
||||
| Container 크기 | 내용 기준(`width: max-content` + max-width = `min(내용, 최대 너비)`). `fit-content`는 shadcn `left: 50%` 배치에서 가용 폭이 화면 절반이 되어 쓰지 않는다(2026-10-06) | Confirmed D-088 |
|
||||
| max-width | `100vw − 2 × edge(16px)` | Confirmed 공식 / edge는 REVIEW |
|
||||
| max-height | `100dvh − app header − app footer − 2 × edge` | Confirmed 공식, header/footer 기본 0 |
|
||||
| 위치 | 가용 영역(앱 header·footer 제외) 가운데 | Confirmed |
|
||||
| Background / 글자 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` | Confirmed reuse |
|
||||
| Border | 1px solid `#DEDBD2` | 두께 Confirmed / 색 Temporary |
|
||||
| Radius | 8px (Container만) | Confirmed |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / 0.2)` | Confirmed (Dialog-local) |
|
||||
| Container padding | 0 (영역별 padding) | 구조 |
|
||||
| Header | padding `24px 0 15px` + 아래 1px divider `#DEDBD2`(= ui_viz `24/24/16`, 선은 16px 안쪽 마지막 1px), 좌우 margin 24px(inset 24px) | Confirmed 위치·두께 / 색 Temporary |
|
||||
| Title ↔ Description | gap 8px (Description이 없으면 없음) | Confirmed |
|
||||
| Title | 20px / 28px / 700, `fg.primary` | Confirmed (Dialog-local) |
|
||||
| Description | 14px / 20px / 400, `fg.secondary` | 크기 Confirmed / 색 PARTIAL |
|
||||
| Body | padding 24px 사방, 16px / 24px / 400, 첫·마지막 child margin 0 | Confirmed |
|
||||
| Footer(Actions) | padding `16px 24px 24px`, gap 8px, 오른쪽 정렬, 위 divider 없음 | 현재 HM 구현 |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere` | Confirmed |
|
||||
| Close 버튼 | shadcn 기본 (ghost `icon-sm` 28px, 오른쪽 위 8px 안쪽, `XIcon`, sr-only "Close") | ui_viz 미정의 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Closed / Open | Radix `data-state` (`open`/`closed`). Open 시 modal, 배경 inert, scroll lock | Radix |
|
||||
| Open animation | shadcn 기본(fade + zoom, 100ms) | ui_viz motion TBD → shadcn 유지 |
|
||||
| Focus | 전역 focus ring은 ui_viz TBD. 조합한 Button이 각자의 focus를 가짐 | TBD |
|
||||
| Long Content | Header·Footer 고정, Body만 세로 scroll, 짧으면 scrollbar 없음 | Confirmed |
|
||||
| Wide Content | Body 안 가로 scroll, 페이지 가로 overflow 없음 | Confirmed |
|
||||
| Confirmation / Destructive | 컴포넌트 상태가 아니라 Button 조합: 일반 확인은 취소 `cancel`+`solid` / 확인 `primary`+`solid`, 삭제 확인은 취소 `cancel`+`outline` / 삭제 `destructive`+`outline`. 같은 세트의 appearance·size 통일, Brand 금지 | HM 사용자 확정 Button 세트 규칙 |
|
||||
|
||||
Loading, Selected, Disabled 등 Dialog 자체의 다른 상태는 ui_viz에 없어 만들지 않았다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Dialog, DialogTrigger, DialogPortal, DialogOverlay, DialogContent,
|
||||
DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose:
|
||||
shadcn / Radix API 그대로 (open, defaultOpen, onOpenChange, modal, asChild, className ...)
|
||||
|
||||
DialogBody:
|
||||
(신규) <div data-slot="dialog-body"> — ui_viz Body region
|
||||
|
||||
DialogClose:
|
||||
shadcn: 항상 data-slot="dialog-close"
|
||||
→
|
||||
HM: asChild일 때는 data-slot을 붙이지 않음 (자식 Button의 data-slot="button" 유지)
|
||||
```
|
||||
|
||||
- `DialogContent`의 `showCloseButton`(기본 true), `DialogFooter`의 `showCloseButton`(기본 false)은 shadcn 그대로.
|
||||
- 새 prop(`size`, `variant`, `destructive`, `loading`, `placement`, `dismissible`)은 근거가 없어 만들지 않았다.
|
||||
- `DialogBody`를 추가한 이유: ui_viz가 Body를 필수 region이자 유일한 scroll owner로 정의하기 때문이다. Body 없이도 동작하지만(자식이 그대로 놓임) 이 경우 Body padding과 내부 scroll이 적용되지 않는다.
|
||||
- `DialogClose` 변경 이유: shadcn은 `DialogClose asChild`의 `data-slot="dialog-close"`가 자식 `<Button>`의 `data-slot="button"`을 덮어써 HM Button 스타일(`[data-slot="button"]`)이 사라진다. Dialog Actions는 Button 조합이 규격(§4)이라 Dialog 쪽에서 고쳤다.
|
||||
- Actions의 Button에는 appearance를 명시한다. Button 기본값은 Brand이며, Destructive는 Outline만 지원한다. [Button 허용 조합·기본값 주의](button.md#61-허용-role--appearance-ai-reference)를 따른다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/dialog.css` (`--hm-dialog-*`), `packages/ui/src/theme.css`에서 import.
|
||||
|
||||
- 재사용한 공통 값: `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-secondary`, `--hm-sem-stroke-input-default`(`#dedbd2`).
|
||||
- Dialog 전용 값: `--hm-dialog-radius`(8px), `--hm-dialog-shadow`, `--hm-dialog-background`, `--hm-dialog-text`, `--hm-dialog-border-width|color`, `--hm-dialog-divider-color`, `--hm-dialog-padding`(24px), `--hm-dialog-header-padding-bottom`(16px), `--hm-dialog-header-gap`(8px), `--hm-dialog-actions-gap`(8px), Title/Description/Body 글자 크기·줄 높이·굵기, `--hm-dialog-description-color`, `--hm-dialog-edge`(16px, REVIEW).
|
||||
- 소비 화면이 설정하는 Web Mapping 이름: `--hm-dialog-app-header-height`, `--hm-dialog-app-footer-height` (기본 0). ui_viz도 Canonical token이 아니라고 명시한다.
|
||||
- Primitive 신규 추가 없음.
|
||||
- Card(radius 4px, border `#dedbd2`)·Input·Button과 값이 겹쳐도 alias하지 않는다. 8px radius는 ui_viz가 Dialog container에만 허용하고 다른 곳으로 확장하지 말라고 명시한다.
|
||||
- Overlay·Close 버튼·animation은 token을 만들지 않았다(shadcn 기본).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조 전부를 그대로 둔다: Portal(`DialogPortal`), Overlay, modal 동작과 배경 `aria-hidden`/inert, focus trap, 열릴 때 initial focus(Radix 기본), 닫힐 때 trigger로 focus 복귀, Escape 닫기, outside interaction 처리, `role="dialog"`와 `aria-labelledby`/`aria-describedby` 연결(`DialogTitle`/`DialogDescription`), Tab 순환, body scroll lock, controlled/uncontrolled(`open`/`defaultOpen`/`onOpenChange`), `asChild`, Close 동작(`DialogPrimitive.Close` 유지, `setOpen` 직접 호출 없음), Close 버튼의 sr-only 레이블과 focus-visible, 열림/닫힘 animation class, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `DialogContent`에서 `grid`, `w-full max-w-[calc(100%-2rem)] sm:max-w-sm`, `gap-4`, `rounded-xl`, `bg-popover`, `p-4`, `ring-1`, `text-popover-foreground`를 제거하고 theme CSS로 이전: 내용 기준 크기(`max-content` + max-width), 최대 너비·높이 공식, 8px radius, 1px border, shadow, 흰 표면.
|
||||
- Header 아래 inset divider, Header·Body·Footer 간격 24/16/8, 타이포.
|
||||
- `DialogFooter`의 `-mx-4 -mb-4`, `border-t`, `bg-muted/50`, `rounded-b-xl`, `p-4`, `gap-2` 제거(경계·배경 없는 Actions 영역).
|
||||
- `DialogBody` 추가, `DialogClose asChild`의 data-slot 처리.
|
||||
- `hm-dialog` class 제거. Demo의 `.hm-dialog-summary`(Dialog 안 내용 레이아웃)는 Demo 전용으로 남겼다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Dialog")이 기본 sidebar의 유일한 Dialog 항목이다 → 1뎁스. overlay라 StateBoard를 쓰지 않고, 트리거 버튼 6개(기본, 긴 내용, 넓은 표, 본문 스크롤, 삭제 확인, 폼)가 각각 실제 Dialog를 연다.
|
||||
- 본문 스크롤은 ui_viz "세로 스크롤 · 본문만"과 같은 조건(앱 공통 Header 70px / Footer 48px 제외)이다. Portal 때문에 앱 높이는 trigger wrapper가 아니라 `DialogContent`의 `style`로 `--hm-dialog-app-header-height`/`--hm-dialog-app-footer-height`를 넘긴다.
|
||||
- verification(`tags: ["verification"]`, 닫힌 채가 아니라 처음부터 열림): `Open`, `Long Content`, `Wide Content`, `Body Scroll`, `Delete Confirmation`.
|
||||
- pseudo-state helper로 열린 상태를 흉내 내지 않는다.
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
추가 사용자 결정 없음. (Radius 8px의 2026-09-17 USER_DECISION은 ui_viz 쪽 기록이다.)
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Overlay**: Black 42%는 REFERENCE_ONLY, 색·opacity는 TBD. 현재 shadcn 기본 `black/10` + blur를 쓰며 ui_viz는 blur를 정의하지 않는다. DECISION_REQUIRED.
|
||||
- **외곽선·Header divider 색**: `#DEDBD2`는 Temporary, Token relation TBD(DIALOG-10 FOLLOW_UP).
|
||||
- **초기 focus**: ui_viz(DIALOG-07, 2026-09-17)는 미지정 시 Dialog 자체가 initial focus를 받는다고 정했다. 현재는 Radix 기본(첫 tabbable 요소; 대표 Story에서는 "취소")이다. Radix 동작을 바꾸지 않기 위해 구현하지 않았다. DECISION_REQUIRED.
|
||||
- **Close(X) 버튼**: ui_viz에 규격이 없다. shadcn 기본이며 긴 Title이 버튼과 겹칠 수 있는지 확인하지 않았다.
|
||||
- **좁은 화면 Actions 배치**: ui_viz TBD. shadcn(`flex-col-reverse sm:flex-row`)을 유지.
|
||||
- **Description 전경**: PARTIAL. **edge 16px**: REVIEW. **motion·z-index·focus ring**: TBD.
|
||||
- **ui_viz FOLLOW_UP**: Alert Dialog·non-dismissible(DIALOG-03), Nested Dialog(DIALOG-04), Select portal 조합과 업무 Dialog 검수(DIALOG-09), `heading-md` 600→700(DIALOG-11). 현재 Alert Dialog는 별도 Component로 구현되어 있다(`alert-dialog.md`). non-dismissible/Nested 조합의 별도 검수는 여전히 보류다.
|
||||
- 공통 header/footer 높이 변수는 소비 화면이 연결해야 하며 이번에 실제 화면에 연결하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/dialog.tsx
|
||||
packages/ui/src/theme/dialog.css
|
||||
packages/ui/src/theme.css (dialog.css import)
|
||||
src/components/dialog.stories.tsx
|
||||
src/App.tsx (Demo Dialog: hm-dialog 제거, DialogBody 사용)
|
||||
src/hm-theme.css (.hm-dialog 규칙 제거, .hm-dialog-summary 유지)
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
> HISTORICAL_RECORD: 아래 측정값은 각 검증 당시 기록이다. 현재 Footer top padding은 §3의 16px이며 과거 크기·padding 측정값을 현재값으로 덮어쓰지 않는다.
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook 실제 상호작용(1280×800): 트리거 클릭으로 열림, Escape로 닫힘, 닫힌 뒤 trigger로 focus 복귀, Close(X) 클릭으로 닫힘 후 trigger focus 복귀, `role="dialog"`·`aria-labelledby`·`aria-describedby`, body scroll lock(`data-scroll-locked`) 확인.
|
||||
- computed style(기본 Dialog): background `rgb(255,255,255)`, border 1px `rgb(222,219,210)`, radius 8px, shadow `0 0 12px rgba(0,0,0,.2)`, 크기 344×227(fit-content), 화면 정중앙, max-width 1248px(=1280−32)·max-height 768px(=800−32), Title 20px/28px/700 `rgb(10,34,29)`, Description 14px/20px/400 `rgb(110,103,89)`, Title↔Description 8px, Header margin-inline 24px·padding 24/16·divider 1px, Body padding 24px·16px/24px, Footer padding `0 24px 24px`·gap 8px·border 0·배경 투명, Close 28×28 오른쪽·위 9px(8px + 외곽선 1px).
|
||||
- Long Content: Dialog 730px(viewport 안), Header·Footer 보임, Body만 scroll(client 613 / scroll 1712), 페이지 가로 overflow 없음. Wide Content: Body 가로 scroll, 페이지 가로 overflow 없음.
|
||||
- Actions: 취소 Gray Solid(`rgb(118,118,118)`/`rgb(90,90,90)`), 삭제 Red Outline(`rgb(211,47,47)`) 적용 확인(`DialogClose` 수정 후).
|
||||
- Story: `All` + verification 4개 ID 확인. 기본 sidebar는 `All` 하나만 보이는 1뎁스 구조(실제 사이드바 화면은 확인하지 못함).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): REGRESSION 2건 수정 — ① `fit-content`가 shadcn `left: 50%` 배치에서 화면 절반만 가용 폭으로 받아 390px에서 Dialog가 195px로 좁아짐 → `width: max-content` + max-width, ② Header가 divider 1px만큼 큼(97px) → bottom padding에서 divider 두께를 뺌(96px). 수정 후 Header 96px·기본 높이 228px이 ui_viz와 같고, 본문 스크롤 Dialog는 3개 viewport 모두 ui_viz와 같은 크기·위치(1280: 580×750 @350,86 / 390: 358×750 @16,86). Tab·Shift+Tab 순환, `aria-labelledby`/`aria-describedby`, scroll lock, Escape·액션·X 닫기 후 trigger focus 복귀 확인(1280/390).
|
||||
- 의도적 차이(유지): Overlay(ui_viz 42% REFERENCE_ONLY), Close X(ui_viz 미정의), 640px 미만 Footer 세로 stack(ui_viz TBD), 초기 focus(Radix 기본 = 첫 focus 가능 요소, ui_viz는 Title/Cancel).
|
||||
- 미확인: Demo(App)의 Dialog 열림 시각 확인, Select를 Dialog 안에 둔 조합.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Drawer
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Drawer 고유의 API·Token·상태·결정만 기록한다. Dialog·Sheet와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Drawer = **vaul** (`vaul@1.1.2`, 이번에 `@hm-design/ui` dependency로 추가) + Radix Dialog |
|
||||
| 대표 Story | `Components/Overlay/Drawer` → `All` (이름 "Drawer") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Drawer가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog의 접근성 원칙과 `asChild` 처리 규칙은 재사용하고 수치는 현재 HM Semantic으로 새로 정했다.
|
||||
- **구현 방식**: shadcn Drawer 구조(vaul)를 유지하고 theme CSS로 HM 스타일 연결. drag·snap·dismiss는 vaul 기본이며 재구현하지 않았다.
|
||||
- **vaul 추가 이유**: shadcn 공식 Drawer가 vaul 기반이다(Radix에 Drawer가 없음). 프로젝트에 없던 dependency라서 추가했다(`npm install vaul --workspace @hm-design/ui`).
|
||||
- **Sheet와의 역할 구분**: Drawer = 손가락/포인터로 끌어 닫는 하단 중심 임시 panel(handle 포함), Sheet = 버튼으로 닫는 edge panel.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-stroke-input-default`, 화면을 향한 한 변 |
|
||||
| Radius | 8px, 화면 안쪽 두 모서리(bottom이면 위 두 모서리) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| bottom / top | 폭 100%, 높이 내용 기준, 최대 `85dvh` |
|
||||
| left / right | 폭 `min(400px, 100vw − 48px)`, 높이 100% |
|
||||
| Handle(bottom만) | 48 × 4px 알약, `--hm-sem-stroke-default`, 위 12px |
|
||||
| Padding | 24px |
|
||||
| Header | padding `24 24 16`, gap 8px |
|
||||
| Title | 18 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary` |
|
||||
| Body | 14 / 20 / 400, 좌우 padding 24px, 유일한 scroll 영역 |
|
||||
| Footer | padding `16 24 24`, gap 8px, 세로 쌓임(주 action이 위, shadcn 기본) |
|
||||
| Close(X) | 없음(handle drag, Overlay 클릭, Escape, `DrawerClose`) |
|
||||
| Overlay | shadcn 기본(`bg-black/10` + blur) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | vaul `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock |
|
||||
| Drag | handle 또는 panel을 아래로 끌어 닫음(vaul 기본). 내부 scroll 영역이 있으면 vaul이 scroll과 drag를 구분 |
|
||||
| Focus | 열릴 때 첫 tabbable로 이동, Tab 순환, 닫힐 때 trigger로 복귀 |
|
||||
| Long Content | Header·Footer 고정, Body만 scroll |
|
||||
|
||||
## 5. API
|
||||
|
||||
vaul `Drawer.Root` props 그대로(`direction`, `snapPoints`, `dismissible`, `shouldScaleBackground`, `modal`, `open`/`onOpenChange` …). export: `Drawer`, `DrawerTrigger`, `DrawerPortal`, `DrawerOverlay`, `DrawerClose`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerTitle`, `DrawerDescription`.
|
||||
|
||||
- `Drawer`: **`autoFocus` 기본값을 true로 둔다**(vaul 기본은 false). vaul 기본값은 Radix의 초기 focus를 취소해, 열린 뒤에도 focus가 aria-hidden 처리된 trigger에 남고 Tab이 Drawer 밖으로 나간다(실측). `autoFocus={false}`로 vaul 동작을 되돌릴 수 있다.
|
||||
- `DrawerContent`: 방향은 root의 `direction`(기본 bottom), `data-vaul-drawer-direction`으로 출력.
|
||||
- `DrawerBody`(신규): scroll owner.
|
||||
- `DrawerClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙).
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/drawer.css` (`--hm-drawer-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `max-height`, `width`, `edge-gap`, `padding`, `header-gap`, `footer-gap`, `handle-width|height|color|margin-top`, `title-*`, `description-*`, `body-*`. Semantic과 Theme palette만 참조하며 `--hm-sheet-*`·`--hm-dialog-*`를 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / vaul / Radix에서 유지한 항목
|
||||
|
||||
vaul의 drag·snap·dismiss·body 처리와 `data-vaul-*` 속성, Radix의 Portal·focus trap·Escape·ARIA·focus 복귀, 방향별 position class, `data-slot`, `cn(..., className)`, handle 요소(bottom 방향에서만 표시), Overlay.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-background`, 방향별 `rounded-*`/`border-*`/`max-h-[80vh]`, handle `h-2 w-[100px] bg-muted`, Header/Footer `p-4` 제거 → theme CSS.
|
||||
- `Drawer`의 `autoFocus` 기본 true(위 §5).
|
||||
- `DrawerBody` 추가, `DrawerClose asChild` data-slot 처리.
|
||||
- Handle을 `data-slot="drawer-handle"` 요소로 구분해 token으로 스타일.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Drawer"): 트리거 3개(기본, 폼, 긴 내용)가 각각 실제 Drawer를 연다. 핸들을 끌어 닫을 수 있다.
|
||||
- verification: `Open`, `Form`, `Long Content`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(85dvh, 18px Title, handle 크기, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay, drag/transition Motion은 vaul·shadcn 기본 유지. `shouldScaleBackground`는 쓰지 않았다.
|
||||
- snap points 사용 예는 만들지 않았다(API는 vaul 그대로 사용 가능).
|
||||
- left/right/top 방향의 시각은 CSS에 두었으나 Story로는 검증하지 않았다(bottom만 사용 예).
|
||||
- Drawer와 Sheet의 사용 경계 가이드는 후속.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/drawer.tsx
|
||||
packages/ui/src/theme/drawer.css
|
||||
packages/ui/package.json (dependency: vaul)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/drawer.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기, focus가 "확인"으로 이동(`autoFocus` 적용 전에는 trigger에 남음), Tab 3회 후에도 Drawer 안(focus trap), Escape 닫힘과 trigger 복귀, handle을 아래로 drag해 닫힘과 focus 복귀, 바깥(Overlay) 클릭 닫힘.
|
||||
- 390×844 긴 내용: Drawer 높이 717px(85dvh), Body만 scroll, Footer가 viewport 하단, 가로 overflow 없음, Footer 버튼 세로 쌓임.
|
||||
- computed style: radius `8px 8px 0 0`, border-top `rgb(222,219,210)`, handle 48×4 `rgb(154,146,132)`.
|
||||
- 미확인: 실제 터치 drag(마우스 drag만 확인), snap points, scroll 영역 안에서 drag와 scroll 구분, 닫기 animation 시각, left/right/top 방향 시각.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Dropdown Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Dropdown Menu 고유의 API·Token·상태·결정만 기록한다. Floating 계열 공통 사항은 §14.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Dropdown Menu (Radix `DropdownMenu`) |
|
||||
| 대표 Story | `Components/Overlay/Dropdown Menu` → `All` (이름 "Dropdown Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Dropdown Menu 규격이 없다(인벤토리에 "상세 디자인·API 보류"). Select의 option 목록 패턴(높이·hover 면)만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(surface, fg, stroke, destructive)와 Select option의 highlight 면(shadcn `accent`)에 맞춰 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Dropdown Menu 구조를 유지하고 theme CSS로 HM 스타일을 연결. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 | 비고 |
|
||||
|---|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` (White) | Semantic |
|
||||
| Border | 1px `--hm-sem-stroke-input-default` | Dialog·Card와 같은 윤곽색 |
|
||||
| Radius | `--hm-radius-container` (4px) | 공통 container radius |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` | HM의 기존 floating 그림자 |
|
||||
| Content padding | 4px | |
|
||||
| Item | min-height 36px, padding-x 12px, gap 8px, radius 2px | |
|
||||
| Typography | 14 / 20 / 400 | Label·Shortcut은 12px (Label 12/18/500) |
|
||||
| 전경 | `--hm-sem-fg-primary` / Label·Shortcut `--hm-sem-fg-secondary` | |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, Content 폭 전체 | |
|
||||
| Check / Radio | 16px indicator 열(12px 안쪽), shadcn `CheckIcon`, 8px 채운 원 | shadcn 아이콘 유지 |
|
||||
| Sub menu chevron | shadcn `ChevronRightIcon` | |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 위 기본 규격 |
|
||||
| Highlighted (`data-highlighted`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 전경 `--accent-foreground` (포인터 hover와 키보드 이동 공통) |
|
||||
| Disabled (`data-disabled`) | shadcn `opacity-50`, 포인터 무시, 키보드 이동에서 제외 |
|
||||
| Checked / Selected | `ItemIndicator`가 check 또는 원을 표시 (Radix `data-state`) |
|
||||
| Sub menu open | SubTrigger `data-state="open"`이 Highlighted와 같은 면 유지 |
|
||||
| Destructive (`variant="destructive"`) | 전경 `--hm-sem-action-destructive`, Highlighted 면 `--hm-color-action-destructive-hover` |
|
||||
| Focus | 항목 자체 outline 없음(`outline-hidden`), Highlighted 면이 focus 표시 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Dropdown Menu props 그대로. export: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuLabel`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent`, `DropdownMenuPortal`.
|
||||
|
||||
- `DropdownMenuContent`: `sideOffset` 기본 4, Radix `align`·`side`·collision props.
|
||||
- `DropdownMenuItem`: shadcn `variant="default" | "destructive"`, `inset`. 출력 `data-variant`.
|
||||
- Label·CheckboxItem·RadioItem·SubTrigger: `inset`.
|
||||
- 출력 속성: `data-slot="dropdown-menu-*"`, Radix `data-state`·`data-highlighted`·`data-disabled`·`data-side`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/dropdown-menu.css` (`--hm-dropdown-menu-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `item-height`, `item-padding-x`, `item-gap`, `item-radius`, `font-size`, `line-height`, `font-weight`, `highlight-background`, `highlight-text`, `destructive-text`, `destructive-highlight-background`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`. Semantic 또는 Theme palette만 참조하고 다른 컴포넌트의 Component Token은 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menu`, `menuitem*`)·키보드(방향키, Home/End, Enter/Space, 오른쪽 방향키 sub menu, Escape, typeahead)·focus 복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, open/close animation(`data-open:`/`data-closed:` class), `z-50`, lucide 아이콘.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 색·`rounded`·`shadow`·`p-1`·`px-2 py-1.5`·`text-sm` 등 시각 class와 `focus:bg-accent` 계열을 제거하고 theme CSS + `data-highlighted`로 대체.
|
||||
- Radio 표시를 별도 아이콘 import 대신 CSS 크기 token을 쓰는 원(`data-slot="dropdown-menu-radio-dot"`)으로 구현.
|
||||
- Check / Radio indicator span에 `data-slot="dropdown-menu-item-indicator"` 추가(위치·크기 token용).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Dropdown Menu"): 트리거로 실제 메뉴를 연다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive).
|
||||
- verification: `Open`(열린 상태, fullscreen).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(36px item, 12px padding, 4px content padding, 그림자 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Disabled opacity는 shadcn 기본 50 유지(전역 Disabled 규칙 없음).
|
||||
- Hover/Highlight 면은 Select와 같은 공통 Accent 45% 정책을 쓴다.
|
||||
- icon 크기·check 위치(시작 위치) 등 icon 세부는 shadcn 기본 유지.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation을 유지.
|
||||
- 자체 scroll 처리(긴 목록)는 `max-h` available height + overflow로 둔다. 별도 규격 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/dropdown-menu.tsx
|
||||
packages/ui/src/theme/dropdown-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/dropdown-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Enter로 열기, 방향키 이동(disabled 항목 건너뜀), Enter로 Checkbox 토글, Radio 선택 시 닫힘과 trigger focus 복귀, 오른쪽 방향키로 Sub menu 열림과 첫 항목 focus, Escape로 sub → menu 순서 닫힘과 trigger focus 복귀.
|
||||
- computed style: Surface white, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`, item 36px/14·20, Highlighted `rgb(189,223,212)`.
|
||||
- 미확인: Home/End·typeahead 입력, 실제 pointer hover, 좁은 viewport collision 시각.
|
||||
|
||||
## 14. Floating 계열 공통
|
||||
|
||||
Popover·Tooltip·Hover Card와 surface(White, 1px pale border, 4px radius, 12px glow) 값이 같지만 각각 `--hm-<component>-*`를 둔다. 공통 Semantic 승격 후보는 `docs/components/popover.md` §14에 함께 기록했다.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Field / Form Field
|
||||
|
||||
**보류 — 컴포넌트를 만들지 않았다.** 현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.7), 이 문서는 보류 이유와 소비 조합 기준만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | **보류** (코드·Story 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | 해당 없음 |
|
||||
| 대표 Story | 없음 |
|
||||
|
||||
## 2. 적용 근거 (보류 이유)
|
||||
|
||||
- 외부 ui_viz Reference (ZIP 미포함) `docs/components/field-semantics.md`: "COMMON SEMANTIC RELATIONSHIP / NON-GEOMETRY-OWNER … **새 Field Component/API를 정의하지 않는다.**" 높이·gap·inset·색·text role·state paint를 공통 Field에 넣지 않으며 DOM/ARIA 연결도 "공통 Field 구현을 요구하지 않는다".
|
||||
- Field 관계(Label → Control → Message)는 컴포넌트별 owner가 소유한다: Input §5·§13·§15·§17.1, Textarea §1·§3·§4, Select.
|
||||
- `프로젝트-지침.md` §6.7: Field/Label 컴포넌트 도입은 별도 결정이며 Input/Textarea/Select에 Field wrapper를 다시 넣지 않는다.
|
||||
- Checkbox Group의 Label/Heading, error slot 계층, helper/error 우선순위는 ui_viz Pattern §4–5에서 `TBD`다.
|
||||
- 근거 없이 Form 시스템을 설계하지 않는다.
|
||||
|
||||
## 3~10. 해당 없음
|
||||
|
||||
구현이 없으므로 기본/상태 규격, API, Token, 유지·변경 항목, Storybook, 사용자 결정은 없다.
|
||||
|
||||
## 11. 소비 조합 기준 (구현이 아니라 기록)
|
||||
|
||||
현재 Input/Textarea Story가 쓰는 조합이며 ui_viz 값이다.
|
||||
|
||||
| 항목 | 값 | 출처 |
|
||||
|---|---|---|
|
||||
| 순서 | Label → Control → Helper/Error | input.md §5, textarea.md §1 |
|
||||
| Label | `<Label>` 컴포넌트(14/20/500, `fg.secondary`) | `docs/components/label.md` |
|
||||
| Field gap | 8px (`input.field.gap`, `OWNER_CONFIRMED_REVISION`) — `space.*`로 승격하지 않음 | input.md §8 |
|
||||
| Helper | 12 / 18 / 400, `fg.secondary` | input.md §9 |
|
||||
| Error | 11 / 18 / 400, `#D32F2F` + `aria-invalid` + `aria-describedby`, 색 단독 전달 금지 | input.md §9, §15 |
|
||||
| Required | 별표 `#D32F2F`, `aria-hidden`; 의미는 native `required` | input.md, TEXTAREA-07 |
|
||||
| 라벨 위치 | 등록·수정 폼은 상단(gap 8), PC 조회·필터는 좌측 라벨(라벨↔입력 12, 묶음 사이 24) | input.md §17.1 |
|
||||
|
||||
보류 해제 조건: ui_viz가 Field를 Component/Pattern으로 확정하거나 사용자가 별도 결정으로 도입을 지시할 때. 이 경우에도 Input/Textarea/Select 내부에 구조를 넣지 않고 composition 컴포넌트로 추가한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
없음 (`docs/components/label.md`, `input.md`, `textarea.md` 참조).
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
해당 없음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Hover Card
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Hover Card 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Hover Card (Radix `HoverCard`) |
|
||||
| 대표 Story | `Components/Overlay/Hover Card` → `All` (이름 "Hover Card") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Hover Card 규격이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Popover와 같은 floating surface이며 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Hover Card 구조를 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **역할 구분**: Tooltip = 짧고 즉각적인 설명, Hover Card = 제목·설명·metadata를 가진 정보 미리보기(non-interactive 중심), Popover = click으로 여는 panel/form.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` |
|
||||
| 전경 | `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-floating-border-color` |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding | 16px |
|
||||
| Width | 320px (viewport − 16px 이하로 제한), 긴 문자열은 줄바꿈 |
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Position | `sideOffset` 4, `align` center |
|
||||
| Delay | Radix 기본 유지(open 700ms, close 300ms) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`, shadcn open/close animation |
|
||||
| 열림 조건 | trigger hover 또는 keyboard focus |
|
||||
| 유지 | trigger → content로 pointer 이동 시 유지(Radix hover bridge) |
|
||||
| 닫힘 조건 | pointer가 trigger와 content를 떠난 뒤 close delay, blur, Escape |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Hover Card props 그대로. export: `HoverCard`, `HoverCardTrigger`, `HoverCardContent`.
|
||||
|
||||
- `HoverCardContent`: `align` 기본 `center`, `sideOffset` 기본 4. `HoverCard`에 `openDelay`, `closeDelay`.
|
||||
- 출력 속성: `data-slot="hover-card-*"`, Radix `data-state`·`data-side`·`data-align`.
|
||||
- 접근성 주의: Hover Card는 sighted pointer user용 보조 정보이며 keyboard focus로도 열리지만 content 안으로 focus를 옮기지 않는다. 필수 정보나 interactive 요소는 넣지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/hover-card.css` (`--hm-hover-card-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·delay·hover bridge·Escape·focus 열림, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `w-64 rounded-md border p-2.5 shadow-md bg-popover` 등 시각 class 제거 → theme CSS.
|
||||
- 폭 256 → 320px, padding 10 → 16px(Popover와 구분되는 미리보기 폭).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Hover Card"): 기본 preview(제목 / 설명 / metadata), 긴 콘텐츠 edge case.
|
||||
- verification: `Open`(열린 상태).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(320px, 16px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- delay는 Radix 기본. HM delay 규칙은 없다.
|
||||
- arrow 없음(shadcn 기본에 없음).
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
- 제목·metadata 타이포 role은 소비 조합(Story는 shadcn Token 유틸 사용). 전용 slot 컴포넌트 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/hover-card.tsx
|
||||
packages/ui/src/theme/hover-card.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/hover-card.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): hover로 열림, trigger → content 이동 시 유지, 영역 이탈 후 닫힘, keyboard focus로 열림, Escape 닫힘.
|
||||
- computed style: width 320px, white, border `rgb(222,219,210)` 1px, padding 16px, shadow `0 0 12px .2`.
|
||||
- 미확인: open/close delay 시간의 정확한 측정, 긴 콘텐츠 시각, 좁은 viewport collision.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Input
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Input 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-input` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Input (native `<input>`) |
|
||||
| 대표 Story | `Components/Form/Input` → `All` (이름 "Input") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size·padding-x(input.md §24, §16), border/radius/색(§10, §11, D-005), Focus/Error 표현.
|
||||
- **ui_viz owner decision**: size별 value·placeholder 글자(§9, §21 `INPUT_VALUE_ROLE_FOLLOWS_SIZE`).
|
||||
- **HM 사용자 결정**: Disabled opacity `0.6`(2026-10-02, §10).
|
||||
- **shadcn 유지**: native input 동작, `file:` 스타일 class, disabled 포인터/커서 처리.
|
||||
- **ui_viz 미정의**: 아래 §11 참조.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | Height(min) | Padding-x | Font / Line-height |
|
||||
|---|---|---|---|
|
||||
| `small` | 28 | 10 | 12 / 18 |
|
||||
| `medium` | 34 | 12 | 14 / 20 |
|
||||
| `large` (기본) | 42 | 12 | 16 / 24 |
|
||||
|
||||
- 높이는 `min-height`, 한 줄 텍스트는 가운데.
|
||||
- Border 1px(`--hm-sem-stroke-input-default`), radius `--hm-radius-input`, face `--hm-sem-bg-surface`, 글자색 `--hm-sem-fg-primary`, weight 400.
|
||||
- Placeholder: `--hm-sem-fg-placeholder`, `opacity: 1`.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | ui_viz Confirmed |
|
||||
| Focus (`:focus-visible`) | border 1.5px `--hm-sem-stroke-input-focus`, shadow/glow 없음, padding으로 0.5px 보정해 값이 움직이지 않음 | ui_viz Confirmed |
|
||||
| Error (`[aria-invalid="true"]`) | border 1.5px `--hm-sem-action-destructive`, ring 없음 | ui_viz Confirmed |
|
||||
| Error + Keyboard Focus | Destructive border 유지 + HM Focus Semantic 3px / 50% outer ring, `outline: none` | 2026-10-06 사용자 결정 |
|
||||
| Disabled | White face 유지, `opacity: 0.6`, `cursor: not-allowed`, `pointer-events: none` | face: ui_viz Confirmed / opacity: 사용자 결정 |
|
||||
| Readonly (`[readonly]`) | 박스 없음(border 0, padding-x 0, 투명 face), dim 없음, 항상 Large 치수 | ui_viz |
|
||||
| Hover / Active | 정의 없음(별도 스타일 없음) | ui_viz 미정의 |
|
||||
|
||||
Focus ring chain은 `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`다. Error border는 `--hm-input-error-border-color` → `--hm-sem-action-destructive`를 따른다. 일반 Focus / Readonly의 기존 표현은 유지하며, editable Invalid + Focus에서 UA outline 의존을 제거했다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Input:
|
||||
size?: native <input size> (number)
|
||||
→
|
||||
size?: "small" | "medium" | "large" 기본 "large"
|
||||
```
|
||||
|
||||
- native `size`(글자 수 폭)를 대체한다. native 속성이 CSS 폭과 무관해 쓰임이 없다는 판단(지침 §6.7).
|
||||
- 출력 속성: `data-slot="input"`, `data-size`.
|
||||
- 상태는 새 prop이 아니라 native 속성으로 준다: `aria-invalid`, `disabled`, `readOnly`.
|
||||
- Field(Label, Required 별표, Helper/Error 문구)는 Input에 포함하지 않는다. 소비 조합이다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/input.css` (`--hm-input-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`, `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`, `--hm-sem-action-destructive`(Semantic 참조).
|
||||
- Input 전용 값: `--hm-input-height-{sm,md,lg}`, `--hm-input-padding-x-*`, `--hm-input-font-size-*`, `--hm-input-line-height-*`, `--hm-input-state-border-width`(1.5px).
|
||||
- Primitive 신규 추가 없음.
|
||||
- Button의 높이 Token(`--hm-button-height-*`)·Select 높이와 값이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이다.
|
||||
- Disabled opacity는 Token이 아니라 `ui/input.tsx`의 `disabled:opacity-60`으로 적용된다(§11).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
native `<input>` 동작 전체(`type`, `file:` 스타일 class, `placeholder`), `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none`, disabled의 `pointer-events-none`/`cursor-not-allowed`. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `size` API 교체, 기본 Large, `data-size` 추가.
|
||||
- shadcn의 기본 높이/padding/border/ring/dark class 제거 → theme CSS가 담당.
|
||||
- 일반 Focus는 border 1.5px로 표시, Error 단독은 ring 없음. Error + `:focus-visible`만 독립 Focus ring을 표시한다.
|
||||
- Readonly 전용 표현 추가.
|
||||
- `hm-input` class 제거.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Input"): 구역 `sizes`, `default`, `focus`, `error`, `error-message`, `disabled`, `readonly`. Focus는 `play` 함수로 실제 focus를 준다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Disabled`, `Readonly`, `Sizes`("Compare Sizes"), `Small`, `Medium`, `Large`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Input 컴포넌트의 일부가 아니다.
|
||||
- sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**
|
||||
|
||||
- Input Disabled opacity `0.6` 확정.
|
||||
- ui_viz에서 Temporary로 기록된 이력이 있으나, 현재 HM 구현에서는 사용자 결정이 우선한다. 이전의 "Temporary → 미적용" 판단은 이 결정으로 superseded.
|
||||
- 이 값은 Input에 대한 현재 최종값이다. 전체 디자인 시스템 공통 Disabled opacity로 자동 확대하지 않는다.
|
||||
- 근거: Select 작업에서 확정된 Disabled 시각 intent와의 정합. Select Trigger도 `0.6`(select.md 참조).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- ui_viz 미정의: Hover, Active 표현, Error 메시지 컴포넌트, Input 내부 아이콘/clear 버튼.
|
||||
- Disabled opacity `0.6`은 Input 한정 사용자 결정이다. 다른 컴포넌트에 적용하지 않는다.
|
||||
- `file:` 타입 스타일은 shadcn 기본이며 HM 규격 없음.
|
||||
- Readonly에서 Disabled 조합 동작은 별도로 확인하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/input.tsx
|
||||
packages/ui/src/theme/input.css
|
||||
src/components/input.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style: `Components/Form/Input` Disabled `opacity` = `0.6` 확인.
|
||||
- Story: `All`의 구역 7개와 verification Story ID 존재 확인.
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): 대표 Story 7구역의 컨트롤·Label·Error 문구 computed style 일치, placeholder 색, Focus·Error 경계, Error+Focus의 outline 유지, 긴 값 가로 스크롤 일치. Readonly는 ui_viz `<p>` 대신 native `input[readonly]`로 같은 42px·16/24 표시. Error Message·Required Story에 ui_viz처럼 placeholder를 넣음(Story만 변경).
|
||||
- 미확인: 브라우저별 렌더(Chromium 외).
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 대표 `All`과 숨김 `InvalidFocus` 확인. 일반 Focus는 border만 변경, Small/Medium/Large 28/34/42px 및 12/14/16px 유지. Readonly border 0 / padding 0, Disabled opacity 0.6 유지. Border CSS 계약 1.5px, 이 브라우저 환경의 computed border-width는 1px으로 정규화; padding 보정은 11.5px 유지. 기존 non-invalid Readonly의 UA outline 정책은 이번 범위에서 유지.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Label
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Label 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (typography·color만, `hm-label` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Label (Radix `Label`) |
|
||||
| 대표 Story | `Components/Form/Label` → `All` (이름 "Label") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- ui_viz에는 **독립 Label 문서가 없다.** Field Label 역할은 Input·Textarea가 소유한다: `docs/components/input.md` §9(14/20/500, `label-md`), §10·§16(`color.fg.secondary` `#6E6759`, Confirmed), `textarea.md` §2.2·§2.
|
||||
- **ui_viz Confirmed (Input·Textarea Field Label 범위)**: 14 / 20 / 500, 색 `color.fg.secondary`. (Source 13/700은 `CANONICAL_OVERRIDE`로 14/20/500이 확정. D-093 원본 실행본 13/700은 `KNOWN_DEVIATION / FOLLOW_UP`.)
|
||||
- **shadcn/Radix 유지**: 라벨 연결(`htmlFor`/감싸기), 클릭 시 컨트롤 focus, `select-none`, Disabled 연동 class.
|
||||
- Required 표시, Checkbox/Radio option label의 별도 규칙은 ui_viz에 없다 → §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 14 / 20 / 500 |
|
||||
| 색 | `--hm-sem-fg-secondary` (`#6E6759`) |
|
||||
| 배치 | `flex w-fit items-center gap-2` (shadcn 기본, 부모 grid에서 시작 정렬) |
|
||||
|
||||
Label ↔ Control 간격 8px(`input.field.gap`)은 Field 조합의 값이며 Label이 소유하지 않는다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 위 기본 규격 |
|
||||
| Disabled | shadcn 기본: 연결된 컨트롤이 `peer`이고 disabled면 `opacity-50`, `cursor-not-allowed` (또는 상위 `group[data-disabled=true]`) |
|
||||
|
||||
Hover/Focus/Active 스타일 없음.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix `Label` props 그대로 (`htmlFor`, `asChild` 등). 출력 속성: `data-slot="label"`.
|
||||
|
||||
- Required 표시(`*`)를 위한 prop은 만들지 않았다(ui_viz `field-semantics.md` §1: Select/Checkbox/Radio 등에 required API·표시를 새로 추가하지 않는다). 소비자가 `aria-hidden` span으로 조합한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/label.css` (`--hm-label-*`).
|
||||
|
||||
- `--hm-label-font-size`(14px), `--hm-label-line-height`(20px), `--hm-label-font-weight`(500), `--hm-label-text`(→ `--hm-sem-fg-secondary`).
|
||||
- Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Label 동작·접근성, `data-slot`, `cn(..., className)`, `select-none`, `group-data-[disabled=true]`·`peer-disabled` 연동.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `leading-none font-medium text-sm` 대신 theme CSS가 typography·색을 정한다.
|
||||
- `flex` 라벨이 한 줄 전체를 차지하지 않도록 `w-fit`을 추가(ui_viz Label `justify-self: start`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Label"): 구역 `default`(htmlFor), `required`(소비 조합), `disabled`, `long`.
|
||||
- 별도 verification Story 없음(구역이 단순).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 추가 사용자 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 독립 Label 규격 없음. Input·Textarea Field Label 값을 Label 컴포넌트에 적용한 것이다. Checkbox/Radio option label의 전경 role은 ui_viz `TBD`(`field-semantics.md` §3)이므로 해당 컴포넌트 작업에서 따로 확인한다.
|
||||
- Disabled opacity: Input의 "Label·Message 포함 .6"(2026-10-02 Input 한정 결정)은 확대하지 않고 shadcn 기본 0.5를 유지.
|
||||
- Required 표시 색(`#D32F2F`, TEXTAREA-07)과 표시 방식은 소비 조합이다. 컴포넌트 API 없음.
|
||||
- D-093 원본 실행본 13/700: `KNOWN_DEVIATION / FOLLOW_UP`.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/label.tsx
|
||||
packages/ui/src/theme/label.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Label)
|
||||
src/components/label.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(`components-form-label--all`): 14px / 20px / 500, color `rgb(110,103,89)`, Disabled 연결 시 opacity 0.5.
|
||||
- 라벨 클릭 시 연결된 input으로 실제 focus 이동 확인.
|
||||
- 2026-10-02 당시 미확인: 사이드바 화면, 390px 폭, Checkbox/Radio와의 조합. 현재 Checkbox/Radio는 구현되어 있으나 이 과거 검증 기록이 조합 검증 완료를 뜻하지는 않는다.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Menubar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Menubar 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu(`dropdown-menu.md`), Navigation Menu(`navigation-menu.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-06 |
|
||||
| 기반 | shadcn/ui Menubar (Radix `Menubar`) |
|
||||
| 대표 Story | `Components/Navigation/Menubar` → `All` (이름 "Menubar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Menubar 규격이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다. Dropdown Menu에서 확인한 Radix menu 운영 원칙(Radix 상태 속성, highlighted/disabled, submenu, Escape, focus 복귀, Portal/collision, shadcn 아이콘 유지)만 참고하고 token은 참조하지 않았다.
|
||||
- **구현 방식**: shadcn / Radix Menubar 구조 유지, theme CSS로 HM 스타일 연결. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
- **역할 구분**: 데스크톱 menu bar(좌우 키로 상위 메뉴 이동)다. Navigation Menu(목적지 링크)와 접근성 모델이 다르고(`role=menubar`/`menu`), Dropdown Menu(단일 trigger)와도 다르다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Bar | 폭 내용 기준(`fit-content`), 높이 38px, padding 2px, gap 2px, 1px `--hm-sem-stroke-input-default` border, radius 4px, 흰 면 |
|
||||
| 상위 Trigger | padding `0 12px`, radius 2px, 14 / 20 / 500 |
|
||||
| Content / SubContent | 흰 면, 1px `--hm-sem-stroke-input-default`, radius 4px, shadow `0 0 12px rgb(0 0 0 / .2)`, padding 4px |
|
||||
| Item | min-height 36px, padding-x 12px, gap 8px, radius 2px, 14 / 20 / 400 |
|
||||
| Label | 12 / 18 / 500, `--hm-sem-fg-secondary` |
|
||||
| Shortcut | 12px, `--hm-sem-fg-secondary` |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, Content 폭 전체 |
|
||||
| Check / Radio | 16px indicator 열, shadcn `CheckIcon`, 8px 채운 원 |
|
||||
| Sub menu chevron | shadcn `ChevronRightIcon` |
|
||||
| 위치 | Content `sideOffset` 8, `alignOffset` -4, `align` start (shadcn 기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| 상위 Trigger Highlighted / Open | 배경 Accent 100%(`--hm-menubar-trigger-highlight-background` → `--accent`), 글자 `--accent-foreground` |
|
||||
| 상위 Trigger Focus (`:focus-visible`) | 기존 강조 면 위에 독립 3px `--ring` 50% box-shadow ring (`--ring` → `--hm-sem-stroke-input-focus`) |
|
||||
| 상위 Trigger Open + Focus | Accent 100% 면 + Focus ring 동시 표시. Focus가 Open 면을 대체하지 않음 |
|
||||
| Item Highlighted (`data-highlighted`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 글자 `--accent-foreground` (포인터 hover와 키보드 이동 공통) |
|
||||
| Disabled (`data-disabled`) | Item은 shadcn `opacity-50`, 상위 Trigger는 `opacity: 0.6`, 키보드 이동에서 제외 |
|
||||
| Checked / Selected | `ItemIndicator`가 check 또는 원을 표시 (Radix `data-state`) |
|
||||
| Sub menu open | SubTrigger `data-state="open"`이 Highlighted와 같은 면 유지 |
|
||||
| Destructive (`variant="destructive"`) | 글자 `--hm-sem-action-destructive`, Highlighted 면 `--hm-color-action-destructive-hover` |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Menubar props 그대로. export: `Menubar`, `MenubarMenu`, `MenubarTrigger`, `MenubarContent`, `MenubarItem`, `MenubarCheckboxItem`, `MenubarRadioGroup`, `MenubarRadioItem`, `MenubarLabel`, `MenubarSeparator`, `MenubarShortcut`, `MenubarSub`, `MenubarSubTrigger`, `MenubarSubContent`, `MenubarGroup`, `MenubarPortal`.
|
||||
|
||||
- `MenubarItem`: shadcn `variant="default" | "destructive"`(출력 `data-variant`), `inset`. Label·CheckboxItem·RadioItem·SubTrigger도 `inset`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/menubar.css` (`--hm-menubar-*`).
|
||||
|
||||
Bar: `background`, `text`, `border-width|color`, `radius`, `height`, `padding`, `gap`. Trigger: `trigger-*`. Content: `content-background|border-color|radius|shadow|padding`. Item: `item-height|padding-x|gap|radius`, `font-size`, `line-height`, `highlight-*`, `destructive-*`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. `--hm-dropdown-menu-*`는 참조하지 않는다(값이 같아 보이는 부분은 후속 token 정리 후보). 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menubar`, `menu`, `menuitem*`)·키보드 모델(Down/Enter/Space로 열기, Up/Down, Home/End, Left/Right, sub menu, typeahead, Escape)·focus 이동/복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, open/close animation, lucide 아이콘.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 `rounded-lg border bg-background h-9 p-1 shadow-xs`, Trigger `rounded-sm px-2 py-1 text-sm font-medium focus:bg-accent`, Content·Item의 색/radius/padding class를 제거하고 theme CSS + `data-highlighted`로 대체.
|
||||
- Radio 표시를 별도 아이콘 import 대신 CSS 크기 token을 쓰는 원으로 구현(Dropdown Menu와 같은 방식).
|
||||
- Check / Radio indicator span에 `data-slot="menubar-item-indicator"` 추가.
|
||||
- Bar 폭을 `fit-content`로 지정(shadcn의 `flex`는 가로 전체로 늘어난다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Menubar"): 파일(item+shortcut, disabled, separator, sub menu 내보내기, destructive 종료), 편집, 보기(Label, Checkbox, Radio Group)를 가진 실제 bar.
|
||||
- 숨김 verification: `TriggerFocus`, `OpenFocus`. `OpenFocus`는 검수 목적으로 Story의 `play`에서 실제 열린 menu의 Trigger에 focus를 준다. 일반 Story/Production은 Radix 기본대로 메뉴 항목에 focus를 이동한다.
|
||||
- Focus 검수는 일반 Story의 computed style 기준으로 한다. Designer의 `.hm-designer button:focus-visible` 장식 outline을 컴포넌트 ring의 근거로 사용하지 않는다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(38px bar, 12px padding, 36px item, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토. Dropdown Menu와 값이 같은 부분은 공통 Semantic 후보이며 이번에 승격·리팩터링하지 않았다.
|
||||
- **모바일**: 데스크톱 중심 컴포넌트다. 390px에서의 구성(상위 메뉴가 넘칠 때)은 검증하지 않았고, 모바일 menu 패턴은 별도로 정한다.
|
||||
- Disabled opacity: Item은 shadcn 50, 상위 Trigger는 0.6으로 서로 다르다(전역 Disabled 규칙 없음).
|
||||
- Motion은 shadcn 기본.
|
||||
- Menu 안 항목을 링크로 쓰는 예는 정하지 않았다(링크 목적지는 Navigation Menu).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/menubar.tsx
|
||||
packages/ui/src/theme/menubar.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/menubar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Focus 정합: `typecheck`, `build`, `build-storybook`, `git diff --check` 통과. 기존 문서 예시 CSS(`bg-[var(...)]`)와 Storybook chunk 크기 경고 유지.
|
||||
- Theme Designer에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`를 분리 적용한 뒤 **일반 Story**에서 확인: Open + Focus Trigger 면 `rgb(255,211,109)` + Focus 50% / 3px ring. 실제 `:focus-visible`, `data-state="open"` 동시 성립, Designer wrapper 없음, computed outline `none`으로 장식 CSS 영향 배제.
|
||||
- 단독 override: Focus는 Trigger ring만, Accent는 Top Trigger 면과 Item Highlighted(45%)만 변경. Primary/Active는 Menubar 면·ring에 영향 없음. Disabled Trigger shadow 없음·opacity 0.6, Disabled Item opacity 0.5 유지.
|
||||
- 일반 키보드 Enter는 실제 첫 Item에 focus를 이동하며 Trigger는 Accent 100% Open 면 / ring 없음, Item은 Accent 45% Highlighted를 유지한다. Escape는 닫힌 Trigger로 focus를 복귀시키며 독립 ring을 표시한다. Navigation Menu 일반 Story에서도 Accent 면 + 3px Focus ring·좌우 키 이동을 확인했고 소스는 변경하지 않았다.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 키보드(Browser pane): 상위 Trigger에서 Enter로 열기(첫 항목에 focus), ArrowDown/Up으로 이동(disabled 항목 건너뜀), Home/End, 오른쪽 방향키로 Sub menu 열림과 왼쪽 방향키로 Sub menu 닫힘(상위 menu 유지), Sub menu 안에서 오른쪽 방향키로 다음 상위 메뉴 Trigger로 이동, 상위 Trigger에서 오른쪽 방향키로 다음 Trigger 이동, ArrowDown으로 보기 menu 열기, Enter로 Checkbox 토글(menu가 닫히고 Trigger로 focus 복귀, 재오픈 시 `눈금자` checked), Escape로 닫힘과 Trigger focus 유지.
|
||||
- computed style: Bar 38px / radius 4px / border `rgb(222,219,210)` / 흰 면, Trigger 14/20/500, Content radius 4px, shadow `0 0 12px .2`, padding 4px, Item 36px, Highlighted `rgb(189,223,212)`.
|
||||
- 미확인: Radio 선택 키보드 동작(Radix 동일 구조, Checkbox만 확인), 실제 pointer hover, typeahead, 좁은 viewport.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Navigation Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Navigation Menu 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu(`dropdown-menu.md`), Menubar(`menubar.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Navigation Menu (Radix `NavigationMenu`) |
|
||||
| 대표 Story | `Components/Navigation/Navigation Menu` → `All` (이름 "Navigation Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Navigation Menu 규격이 없다. 현재 위치(Current) 의미와 Hover/Focus는 독립이라는 ui-viz의 navigation 안내만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다. Dropdown Menu의 운영 원칙(Radix 상태 속성, Escape, focus 복귀, Portal/viewport)만 참고하고 token은 참조하지 않았다.
|
||||
- **구현 방식**: shadcn / Radix NavigationMenu 구조(Root, List, Item, Trigger, Content, Link, Viewport, Indicator) 유지, theme CSS로 HM 스타일 연결.
|
||||
- **역할 구분**: 목적지 이동(링크) 중심이다. Dropdown Menu(명령)·Menubar(메뉴 바)와 접근성 모델이 다르다(`role=menu`가 아니라 disclosure + link).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 상위 Trigger / Link | 높이 34px, padding `0 12px`, radius 4px, 14 / 20 / 500, `--hm-sem-fg-primary`, 항목 gap 4px |
|
||||
| Trigger indicator | shadcn `ChevronDownIcon` 12px, open 시 180° 회전 |
|
||||
| Content / Viewport 면 | `--hm-sem-bg-surface`, 1px `--hm-sem-floating-border-color`, radius 4px, shadow `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Content | padding 8px, min-width 240px, 폭은 안의 내용이 정함 |
|
||||
| Content 안 Link(목적지) | 세로 배치, padding `8px 12px`, gap 2px, radius 2px. `data-description` 자식은 12 / 18 / 400 secondary |
|
||||
| Indicator(화살표) | shadcn 회전 사각형, `--hm-sem-floating-border-color` |
|
||||
| 열기 | hover(Radix 기본 delay)·click·keyboard. 이동 animation은 shadcn 기본 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover / Focus / Open (Trigger) | 배경 `--accent`, 글자 `--accent-foreground`. Focus는 ring 모양 `0 0 0 3px ring 50%` 추가 |
|
||||
| Link Hover / Focus | Trigger와 같은 면 |
|
||||
| Active Link (`active`) | Radix `data-active` + `aria-current="page"`. 글자 `--hm-sem-action-primary`, weight 700, 배경 `--accent` 45% (hover면 일반 hover 면) |
|
||||
| Disabled Trigger | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, native `disabled` |
|
||||
| Open / Closed | Radix `data-state`, 이동 방향은 `data-motion` (shadcn animation) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix NavigationMenu props 그대로. export: `NavigationMenu`, `NavigationMenuList`, `NavigationMenuItem`, `NavigationMenuTrigger`, `NavigationMenuContent`, `NavigationMenuLink`, `NavigationMenuIndicator`, `NavigationMenuViewport`.
|
||||
|
||||
- `NavigationMenu`: `viewport`(기본 true, shadcn). `false`면 각 Content가 자기 Trigger 아래에 열린다. 출력 `data-viewport`.
|
||||
- `NavigationMenuLink`: `active`(현재 위치), `asChild`로 router Link 조합(router에 의존하지 않음). disabled link는 지원하지 않는다.
|
||||
- `NavigationMenuTrigger`: chevron 내장. `disabled`는 native 속성.
|
||||
- `NavigationMenuIndicator`: 선택 사용(Story의 viewport 예시에서 사용).
|
||||
- 2026-10-06 Visual QA: viewport 모드의 Item은 static으로 두어 Radix List wrapper가 Trigger의 `offsetParent`가 되도록 한다. `viewport={false}`에서만 Item의 relative anchor를 유지한다. Indicator에 임의 위치 보정값을 넣지 않는다. `IndicatorAlignment` verification Story는 서로 다른 label width의 첫·중간·마지막 Trigger와 두 viewport 모드를 검증한다.
|
||||
- 대표 Story의 고정 360/300px 내용은 좁은 화면에서 자연 폭을 사용한다. Desktop 폭도 Story frame container에 제한하며, 모바일 inline Content의 absolute 배치는 Story 범위에서만 적용한다(Production CSS 변경 없음).
|
||||
- 새 prop은 만들지 않았다. Content 안 title/description은 소비 조합이며 `data-description` 속성으로 보조 문구를 표시한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/navigation-menu.css` (`--hm-navigation-menu-*`).
|
||||
|
||||
`height`, `padding-x`, `gap`, `radius`, `font-size|line-height|font-weight`, `text`, `icon-size`, `hover-background|text`, `active-background|text|font-weight`, `background`, `border-width|color`, `surface-radius`, `shadow`, `content-padding`, `content-min-width`, `item-padding|gap|radius`, `description-*`, `disabled-opacity`. Semantic과 Theme palette(`--hm-sem-floating-border-color`)만 참조한다. `--hm-dropdown-menu-*`, `--hm-tab-*`, `--hm-button-*`는 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·동작(hover open delay, Arrow 키와 Tab 이동, Escape 닫힘, focus 복귀, Viewport 크기 측정, Indicator 위치), ARIA(`aria-expanded`, `aria-controls`, `aria-current`), 이동 animation(`data-motion`), viewport/non-viewport 구조, chevron, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `navigationMenuTriggerStyle`(rounded-md, h-9, bg-background, hover/focus/open `bg-accent/50`)과 Content·Viewport의 `bg-popover text-popover-foreground rounded-md border shadow` 등을 제거하고 theme CSS로 이전.
|
||||
- Content 안 Link는 shadcn에서 별도 class로 꾸미던 것을 선택자(`content` 하위 `link`)로 처리.
|
||||
- Active Link 스타일(색·weight·배경)을 정의(shadcn은 `data-[active=true]:bg-accent/50`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Navigation Menu"): `Viewport(기본)`과 `viewport={false}` 두 구성을 한 페이지에 둔다. 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger).
|
||||
- verification: `Default`, `No Viewport`.
|
||||
- Story의 Content 폭은 `style` 인라인으로 지정한다(새 Story 파일의 Tailwind 임의값 클래스가 개발 서버에서 생성되지 않는 일이 있었다).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(34px, 12px padding, Content 240px 최소 폭, 그림자, Active 표현)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- **모바일**: Radix NavigationMenu는 데스크톱 GNB 구조다. 390px에서 Viewport의 폭은 Content 폭(Story에서 360px)을 따라가며 `max-width` 제한이 없어 우측 끝에 닿았다(가로 overflow는 없음). 모바일 navigation(햄버거, Sheet/Drawer 조합 등)은 GNB/Layout 단계에서 별도 패턴으로 검토한다.
|
||||
- disabled link 지원 없음. 필요하면 `aria-disabled` 규칙을 Pagination과 같은 방식으로 후속 검토.
|
||||
- Indicator 사용 여부와 색은 현재 선택 사항이며 별도 규격이 없다.
|
||||
- 열림/닫힘 Motion은 shadcn 기본.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/navigation-menu.tsx
|
||||
packages/ui/src/theme/navigation-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/navigation-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 제품 Trigger/Arrow center 48.8828/49px, 자료 118.6484/119px. 다른 label width(제품/기술 자료/지원 센터)도 Arrow 오차 절댓값 0.45px 이내. inline 모드의 각 Content가 자기 Trigger 아래에 열림 확인. 대표 Story 두 모드의 열린 패널이 viewport 안에 있고 페이지 가로 overflow 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 키보드로 Enter 열림(`aria-expanded=true`), Tab으로 Content 안 첫 링크, 다음 링크로 이동, Escape로 닫히고 Trigger로 focus 복귀, ArrowRight로 다음 상위 항목으로 이동, 마우스 hover로 해당 Content가 열림, 현재 위치 링크가 `aria-current="page"`·`data-active`·weight 700.
|
||||
- computed style: Viewport 흰 면, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`. Content 폭이 안의 목록(360px) 폭에 맞춰 376px.
|
||||
- 390×844: 열린 Viewport 폭 378px, 좌측 16px에서 시작해 오른쪽 끝에 닿음(가로 overflow 없음). 한계는 §11.
|
||||
- 미확인: 닫힘 animation 시각, `viewport={false}` 구성의 열림(코드 경로는 Radix/shadcn 기본), 두 Content 사이 이동 animation 시각, ArrowDown으로 Content 진입.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Pagination
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Pagination 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Pagination (`nav > ul > li > a`, Radix 없음) |
|
||||
| 대표 Story | `Components/Navigation/Pagination` → `All` (이름 "Pagination") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Pagination이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary, action-primary, accent)로 새로 정했다.
|
||||
- **구현 방식**: shadcn Pagination 구조 유지. Presentational / navigation primitive로, page state와 href는 소비자가 관리한다. 링크는 `a`로 유지하고 Button으로 바꾸지 않는다.
|
||||
- **Button과의 관계**: shadcn은 `buttonVariants`를 입혀 Button처럼 그리지만, HM Button은 `data-slot="button"` 기반이라 이를 그대로 쓰면 HM 스타일이 적용되지 않고 Button token도 재사용하면 안 된다. 그래서 `PaginationLink`는 독립 anchor 스타일(`--hm-pagination-*`)로 구현했다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Page link | 34 × 34px(min-width 34), radius 4px(`--hm-radius-control-md`), 14 / 20 / 500, `--hm-sem-fg-primary` |
|
||||
| Previous / Next | 높이 34px, padding `0 12px`, 아이콘 16px, 라벨 `hidden sm:block`("Previous"/"Next", 모바일은 아이콘만) |
|
||||
| 간격 | 목록 gap 4px |
|
||||
| Current Page | 배경 `--hm-sem-action-primary`(Primary Green `#123328`), 글자 `--hm-sem-on-action-primary`(흰색), weight 700 |
|
||||
| Ellipsis | 34 × 34 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover | 배경 `--accent` 45%, 글자 `--accent-foreground` |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` |
|
||||
| Current (`isActive`) | `aria-current="page"`, 위 Current 스타일(hover에도 `Current`로 보이도록 hover 배경 대신 유지) |
|
||||
| Disabled (`aria-disabled="true"`) | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, hover 면 없음, `tabIndex -1`, 클릭 취소(`preventDefault`) |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink`, `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis`.
|
||||
|
||||
- `PaginationLink`: `a` props + `isActive`(현재 페이지), `size` `"default" | "icon"`(기본 `icon`; shadcn은 Button size, HM은 `default`/`icon` 둘만). 출력 `data-slot="pagination-link"`, `data-active`, `data-size`.
|
||||
- disabled: 별도 prop 없이 `aria-disabled`를 쓴다. `aria-disabled="true"`이면 컴포넌트가 `tabIndex=-1`로 두고 클릭(onClick 호출 포함)을 취소한다. anchor에는 native disabled가 없어 class만으로는 접근성이 보장되지 않기 때문에 보완했다.
|
||||
- `PaginationPrevious`/`PaginationNext`: 기본 `aria-label`("Go to previous/next page")과 라벨, `children`으로 교체 가능.
|
||||
- 새 상태 관리, `page`/`total` 같은 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/pagination.css` (`--hm-pagination-*`).
|
||||
|
||||
`gap`, `height`, `min-width`, `padding-x`, `radius`, `font-size|line-height|font-weight`, `icon-size`, `text`, `hover-background`, `hover-text`, `active-background`, `active-text`, `active-font-weight`, `ellipsis-color`, `disabled-opacity`. Semantic만 참조하고 `--hm-button-*` 등 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`nav role="navigation" aria-label="pagination"`, `ul`/`li` 구조, `a` 링크, `isActive`→`aria-current="page"`, Previous/Next 아이콘과 `aria-label`, Ellipsis의 `aria-hidden` + sr-only, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `buttonVariants` 합성을 제거하고 독립 anchor 스타일로 대체(위 §2).
|
||||
- `size`를 shadcn의 Button size 전체에서 `default`/`icon`으로 좁혔다.
|
||||
- `aria-disabled` 처리(tabIndex, 클릭 취소)를 추가했다.
|
||||
- 현재 페이지를 outline 대신 채움(primary)으로 표시.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Pagination"): First page, Middle page, Last page, Long range with ellipsis(1 … 9 10 11 … 20)를 한 페이지에 둔다. 각 Specimen이 local state를 가지고 클릭 시 현재 페이지가 실제로 바뀐다(`#page-N` href + `preventDefault`).
|
||||
- verification: `First Page`, `Last Page`, `Long Range`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
- 2026-10-06 (Pagination Current 원복): 직전 Active Semantic 작업에서 Current를 `--hm-sem-state-active`(Light 2)로 바꾼 변경을 취소하고 이전 값 `--hm-sem-action-primary` / `--hm-sem-on-action-primary`(Primary Green `#123328`, 흰색)로 복원했다. Current + Hover도 Current 색을 유지한다. Hover의 `--accent` 45%는 유지한다. Focus·Disabled·API·구조 변경 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(34px, 4px radius, 12px padding, 채움 current, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- page range(ellipsis 계산)는 소비자 책임이다(Story의 `pageRange`는 예시일 뿐 컴포넌트가 아님).
|
||||
- 언어: Previous/Next 기본 라벨은 shadcn 영어 그대로(`children`으로 교체).
|
||||
- 좁은 화면 compact 표현은 shadcn의 라벨 숨김(`sm` 미만)만 유지. 별도 compact variant 없음.
|
||||
- 페이지 크기 선택, 결과 수 표시는 범위 밖.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/pagination.tsx
|
||||
packages/ui/src/theme/pagination.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/pagination.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane, JS click): page 4 클릭으로 current가 4로 변경, Next로 5, 첫 페이지의 Previous(`aria-disabled`)와 마지막 페이지의 Next 클릭은 상태를 바꾸지 않음. `aria-disabled` 링크는 `tabindex=-1`, opacity 0.6, cursor not-allowed. Current는 `aria-current="page"`, 34×34, 배경 `rgb(18,51,40)`, 글자 white, weight 700.
|
||||
- 구조: `a` 요소, ellipsis 위치(1 … 9 10 11 … 20)와 First/Last 상태 확인.
|
||||
- Responsive: 390×844에서 Previous/Next 라벨이 숨겨져 아이콘만 남고, 20페이지 범위(`1 … 9 10 11 … 20`)가 350px 한 줄에 들어가며 가로 overflow 없음.
|
||||
- 미확인: 실제 키보드 Tab/Enter 순서, pointer hover·focus ring 시각.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Popover
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Popover 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Popover (Radix `Popover`) |
|
||||
| 대표 Story | `Components/Overlay/Popover` → `All` (이름 "Popover") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Popover 규격이 없다(인벤토리 "상세 디자인·API 보류"). 같은 floating surface인 Dialog의 윤곽 의도만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Popover 구조를 유지하고 theme CSS로 HM 스타일을 연결. Popover는 일반 floating panel이며 modal이 아니다(overlay·focus trap 없음).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` |
|
||||
| 전경 | `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-floating-border-color` |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding | 16px |
|
||||
| Width | 288px (viewport − 16px 이하로 제한) |
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Position | `sideOffset` 4, `align` center (기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`, shadcn open/close animation |
|
||||
| Focus | 열릴 때 Radix가 content 안 첫 focus 가능 요소로 이동, 닫힐 때 trigger로 복귀(Escape/Close) |
|
||||
| 바깥 클릭 | 닫힘. focus는 클릭한 곳에 둔다 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Popover props 그대로. export: `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverAnchor`, `PopoverClose`.
|
||||
|
||||
- `PopoverContent`: `align` 기본 `center`, `sideOffset` 기본 4.
|
||||
- `PopoverClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙, Button HM 스타일 보존).
|
||||
- 출력 속성: `data-slot="popover-*"`, Radix `data-state`·`data-side`·`data-align`.
|
||||
|
||||
### 접근성 이름
|
||||
|
||||
`PopoverContent`는 `role="dialog"`로 출력된다. 소비자는 패널의 목적을 나타내는 접근성 이름을 `aria-label` 또는 `aria-labelledby`로 지정한다. Trigger의 텍스트는 패널 이름으로 자동 연결되지 않는다.
|
||||
|
||||
```tsx
|
||||
<PopoverContent aria-label="프로젝트 이름 변경">
|
||||
{/* 입력 폼 */}
|
||||
</PopoverContent>
|
||||
```
|
||||
|
||||
보이는 제목이 있다면 해당 제목의 고유 ID를 연결한다. 같은 화면에서 반복되는 패널은 `React.useId()` 등으로 서로 다른 ID를 생성한다.
|
||||
|
||||
```tsx
|
||||
const titleId = React.useId()
|
||||
|
||||
<PopoverContent aria-labelledby={titleId}>
|
||||
<h2 id={titleId}>프로젝트 안내</h2>
|
||||
<p>일정과 담당자 정보를 확인합니다.</p>
|
||||
</PopoverContent>
|
||||
```
|
||||
|
||||
대표 Story는 정보·폼·정렬 예제 각각에 `aria-label`을 제공한다. 이름 지정은 기존 레이아웃과 Radix의 focus 이동·Escape·닫힘 동작을 바꾸지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/popover.css` (`--hm-popover-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=dialog`, `aria-expanded`, `aria-controls`)·Escape·바깥 클릭 닫힘·focus 이동/복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `w-72 rounded-md border p-4 shadow-md bg-popover text-popover-foreground` 등 시각 class 제거 → theme CSS.
|
||||
- `PopoverClose` 추가 export (shadcn 기본 export에 없는 Radix `Close`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Popover"): 정보 패널, 폼 패널(Input + 취소/저장 `PopoverClose`), `align` start/center/end 비교.
|
||||
- verification: `Open`(열린 상태).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(288px, 16px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- arrow 없음(shadcn 기본에 없음).
|
||||
- 크기 variant 없음. 폭은 `className`으로 확장.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/popover.tsx
|
||||
packages/ui/src/theme/popover.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/popover.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08: 대표 Story의 정보·폼·정렬 3종, 총 5개 `role="dialog"` 접근성 이름을 확인했다. 키보드 Enter·Tab·Escape, 폼 자동 focus, 취소/저장 `PopoverClose`의 닫힘·trigger focus 복귀, 바깥 클릭 닫힘을 Browser에서 확인했다. 공개 API 타입 검사 통과, 최종 검증 Console 오류 없음.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기(focus가 content 안 Input으로 이동), Escape 닫힘과 trigger focus 복귀, 바깥 클릭 닫힘.
|
||||
- computed style: width 288px, white, border `rgb(222,219,210)` 1px, radius 4px, padding 16px, shadow `0 0 12px .2`.
|
||||
- 미확인: 좁은 viewport collision 시각.
|
||||
|
||||
## 14. Floating 계열 공통 (Semantic 승격 완료)
|
||||
|
||||
Dropdown Menu·Popover·Hover Card는 surface(White, 1px pale border, 4px radius, `0 0 12px .2` 그림자)가 같다. 의미(떠 있는 패널의 면·윤곽·그림자)도 같아 공통 Semantic 후보는 아래이며, `default.css`의 `--hm-sem-floating-*`로 승격했고 각 컴포넌트의 `--hm-<component>-*`가 이를 참조한다(Component Token은 유지). Dialog(Dialog 전용 값), Tooltip(dark inverse face)은 같은 면이 아니다.
|
||||
|
||||
- Floating background / border / shadow / radius
|
||||
|
||||
Popover만 다르게 하려면 `--hm-popover-*`만 override한다. Date Picker·Combobox popup은 Popover를 그대로 쓰므로 이 값을 따른다.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Progress
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Progress 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격, 기본 Progress 하나) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Progress (Radix `Progress`) |
|
||||
| 대표 Story | `Components/Data & Display/Progress` → `All` (이름 "Progress") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Progress가 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Progress 구조(Root + Indicator)를 유지하고 theme CSS로 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 높이 8px, radius 9999px, 배경 `--hm-sem-palette-warm-neutral-light` |
|
||||
| Indicator | 배경 `--hm-sem-state-active`(현재 Active와 Progress의 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리), 폭 = value/max 비율(`translateX`), `transition-all`(shadcn 기본) |
|
||||
| 폭 | 부모 폭 100% |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Loading / Complete | Radix `data-state="loading"`(0 ≤ value < max), `"complete"`(value = max) |
|
||||
| Indeterminate | `value`를 주지 않으면 Radix `data-state="indeterminate"`. 현재는 Indicator가 비어 있다(별도 표현 없음) |
|
||||
| Success / Warning / Error | 만들지 않았다(HM 상태색 미확정) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Progress props 그대로. export: `Progress`.
|
||||
|
||||
- `value`, `max`(기본 100), `getValueLabel`, `asChild`. Indicator는 내부에서 렌더하며 별도 export 없음(shadcn 구조).
|
||||
- ARIA: `role="progressbar"`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`(Radix). 접근 가능한 이름은 소비자가 `aria-label`/`aria-labelledby`로 지정.
|
||||
- `variant`, `size`, `status` 같은 prop은 만들지 않았다. 높이나 색을 바꾸려면 `--hm-progress-*` Token을 덮는다(unlayered theme 규칙이라 `className`의 높이 utility는 이기지 못한다).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/progress.css` (`--hm-progress-*`).
|
||||
|
||||
`height`, `radius`, `track-background`, `indicator-background`. Semantic과 Theme palette(`--hm-sem-palette-warm-neutral-light`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·`data-state`, Indicator의 `translateX` 표현(폭 계산은 HM에서 value/max 비율로 보정), `transition-all`, `overflow-hidden`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `h-1 rounded-full bg-muted`, Indicator `bg-primary` class를 제거하고 theme CSS로 이전. 높이를 4px에서 8px로 키웠다.
|
||||
- 트랙색을 `muted`가 아닌 `--hm-sem-palette-warm-neutral-light`로 정했다(`muted`는 베이지 배경에서 거의 안 보임).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Progress"): 0%, 50%, 100%, max=200, Interactive value(`-25`/`+25` 버튼, local state)를 한 페이지에 둔다.
|
||||
- verification: `Zero`, `Middle`, `Full`, `NonDefaultMax`(max=200의 0/50/200/indeterminate). 대표 Story에도 동일 specimen을 둔다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(8px, 트랙색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 상태 variant(success/warning/error)는 HM 상태색 확정 후.
|
||||
- Indeterminate 표현(움직이는 막대)은 없다. Motion 규칙이 정해지면 검토.
|
||||
- 값 label/퍼센트 텍스트는 소비 조합(컴포넌트에 없음).
|
||||
- `prefers-reduced-motion` 대응은 추가하지 않았다(`transition-all`은 shadcn 기본).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/progress.tsx
|
||||
packages/ui/src/theme/progress.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/progress.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 DOM(Browser pane): `role="progressbar"`, `aria-valuemin=0`, `aria-valuemax=100`, `aria-valuenow`가 0/50/100/30(interactive), `data-state` loading/complete, indicator 채움이 0/50/100%.
|
||||
- value 변경: `+25` 클릭으로 `aria-valuenow`가 30 → 55, 이어서 100으로 클램프되고 indicator transform이 `translateX(0%)`, 렌더 후 채움 100%. 0과 max 경계 확인.
|
||||
- computed style: 높이 8px, radius 9999px, 트랙 `rgb(224,218,206)`, indicator `rgb(18,51,40)`.
|
||||
- Responsive(390×844): 트랙이 부모 폭 358px을 따르고 가로 overflow 없음.
|
||||
- 미확인: indeterminate 표시, `max`를 100이 아닌 값으로 쓴 경우(Indicator 계산이 value를 %로 간주하므로 `max≠100`이면 소비자가 환산해야 함).
|
||||
|
||||
### 2026-10-06 정합성 감사 수정
|
||||
|
||||
위 2026-10-02 검증 이력의 max≠100 제한은 해소했다. Indicator는 Radix와 같은 유효 범위/기본 max를 사용하여 value/max 비율로 채운다. max=200, value=50은 ARIA 50/200과 25% 채움이 일치하며 값 없음은 기존 indeterminate 빈 Track을 유지한다.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Radio Group
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Radio Group 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Radio Group (Radix `RadioGroup`) |
|
||||
| 대표 Story | `Components/Form/Radio Group` → `All` (이름 "Radio Group") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Radio 문서가 없다(`RADIO-01` TBD, HOLD, group spacing·전경 role TBD). 확정 규격이 없어 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4. Checkbox와 같은 HM Semantic 체계로 새로 정했다(Component Token은 따로 둔다).
|
||||
- **구현 방식**: Radix RadioGroup(roving focus, 방향키 선택, `role=radiogroup/radio`, `data-state`, `data-orientation`)을 유지하고 theme CSS로 연결. shadcn `CircleIcon`을 inner dot으로 유지.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Outer | 18 × 18px 원, border 1px `--hm-sem-stroke-default`, face `--hm-sem-bg-surface` |
|
||||
| Inner dot | 8px, `--hm-sem-state-active` (shadcn `CircleIcon`, fill) |
|
||||
| Group gap | vertical 12px, horizontal 16px (wrap) |
|
||||
| Orientation | Radix `orientation` prop → `data-orientation`. 기본 vertical(grid), horizontal은 flex |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Unchecked | 위 기본 규격 |
|
||||
| Checked | border `--hm-sem-state-active` + inner dot |
|
||||
| Hover | border `--hm-sem-action-primary` |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 |
|
||||
| Error (`aria-invalid="true"` on item) | border `--hm-sem-action-destructive` |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed` (항목 또는 그룹 `disabled`) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix `RadioGroup`/`RadioGroupItem` props 그대로 (`value`/`defaultValue`/`onValueChange`, `orientation`, `disabled`, `name`, `required`, `loop`, `value` on item). 추가 prop 없음. 출력 속성: `data-slot="radio-group"`, `"radio-group-item"`, `"radio-group-indicator"`. 그룹 이름은 `aria-label` 또는 `aria-labelledby`로 준다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/radio-group.css` (`--hm-radio-*`): `size`, `border-width`, `dot-size`, `background`, `border-color`, `hover-border-color`, `checked-border-color`, `dot-color`, `error-border-color`, `group-gap`, `group-gap-horizontal`. Semantic 참조, Checkbox 등 다른 컴포넌트 Token 미사용. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·roving tabindex·방향키 선택, `data-slot`, `cn(..., className)`, `transition-colors`, `CircleIcon`, focus ring, disabled 커서.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `size-4 border-input text-primary shadow-xs dark:` class 제거 → theme CSS. Group gap `gap-3` 고정 대신 Token + horizontal 레이아웃. Disabled opacity 50 → 60.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `vertical`, `horizontal`, `hover`, `focus`, `error`, `disabled-item`, `disabled-group`. Focus는 `play`로 실제 focus. verification: `Vertical`, `Horizontal`, `Disabled`, `Error`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서(작업 지시). 개별 수치 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 그룹 Label/Heading, Error·Helper 슬롯의 소유와 우선순위(컴포넌트 없음, 소비 조합).
|
||||
- 수치(18px, 8px dot, gap 12/16)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Hit area 확장 미적용.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/radio-group.tsx
|
||||
packages/ui/src/theme/radio-group.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export RadioGroup, RadioGroupItem)
|
||||
src/components/radio-group.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: 18×18, 50% radius, border 1px; Checked border `rgb(18,51,40)`, dot 8px; Unchecked `rgb(154,146,132)`; Error `rgb(211,47,47)`; Disabled opacity 0.6; vertical gap 12px, horizontal `flex` gap 16px.
|
||||
- interaction: 항목 click으로 선택 변경, ArrowUp으로 선택·focus가 이전 항목으로 이동, roving tabindex(선택 항목 0, 나머지 -1) 확인.
|
||||
- 미확인: 실제 pointer hover·focus ring 시각, 사이드바 화면, 390px 폭.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Resizable
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Resizable 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Interactive layout primitive** (panel 크기 조절). Dashboard/Sidebar/persistence를 소유하지 않는다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Resizable = **react-resizable-panels v4** (`^4.14.2`) |
|
||||
| 대표 Story | `Components/Disclosure/Resizable` → `All` (이름 "Resizable") |
|
||||
| 보류 | 아래 §12 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Resizable이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Separator Token은 참조하지 않는다.
|
||||
- **구현 방식**: 라이브러리가 drag, 최소/최대, collapsing, layout 계산, 키보드 resize, pointer hit-target 확장, separator ARIA를 소유한다. HM은 divider와 선택형 grip만 스타일한다.
|
||||
- **v4 주의**: 라이브러리 이름과 API가 v3와 다르다. v3의 `PanelGroup`/`PanelResizeHandle`가 아니라 **`Group`/`Panel`/`Separator`**이고, 크기는 **숫자 = 픽셀, 단위 없는 문자열(`"30"`) 또는 `"30%"` = 퍼센트**다. shadcn 이름(`ResizablePanelGroup`/`ResizablePanel`/`ResizableHandle`)은 유지했다.
|
||||
- **보류**: §12.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Group | `flex h-full w-full`. `orientation` `"horizontal"`(기본) / `"vertical"`. Group·Panel의 inline sizing은 라이브러리 것이라 덮을 수 없다 |
|
||||
| Divider(Separator) | 1px, `--hm-sem-stroke-input-default`. 나란한 panel 사이는 세로선(`aria-orientation="vertical"`), 쌓인 panel 사이는 가로선 |
|
||||
| Pointer target | 라이브러리가 1px divider 둘레로 확장한다(DOM 크기가 아니라 hit test). 8px 떨어진 곳은 잡히지 않았다 |
|
||||
| Grip (`withHandle`) | 12 × 16px, 흰 면, 1px pale border, radius 2px, `GripVerticalIcon` 10px `--hm-sem-fg-secondary`, divider 가운데. 쌓인 panel 사이에서는 90° 회전 |
|
||||
| 크기 | 소비자(Group의 `className`/부모)가 정한다. 컴포넌트가 높이/폭을 강제하지 않는다 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover (`data-separator="hover"`) | divider와 1px halo가 `--hm-sem-stroke-default` |
|
||||
| Dragging (`data-separator="active"`) | divider와 1px halo가 `--hm-sem-action-primary` |
|
||||
| Focus (`:focus-visible`) | divider와 1px halo가 `--hm-sem-stroke-input-focus` |
|
||||
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함) |
|
||||
| Collapsed panel | 라이브러리의 collapse(`collapsible`, `collapsedSize`). `aria-valuenow`가 0 |
|
||||
|
||||
## 5. API
|
||||
|
||||
라이브러리 props 그대로. export: `ResizablePanelGroup`(= `Group`), `ResizablePanel`(= `Panel`), `ResizableHandle`(= `Separator`).
|
||||
|
||||
- `ResizablePanelGroup`: `orientation`, `defaultLayout`, `onLayoutChange`/`onLayoutChanged`, `groupRef`, `disabled`, `resizeTargetMinimumSize`, `id`. 저장(`useDefaultLayout`) 같은 persistence는 라이브러리에 있지만 HM이 래핑하거나 쓰지 않았다.
|
||||
- `ResizablePanel`: `defaultSize`, `minSize`, `maxSize`, `collapsible`, `collapsedSize`, `panelRef`, `onResize`, `id`.
|
||||
- `ResizableHandle`: `withHandle`(shadcn, grip 표시), `disabled`, `disableDoubleClick`, 그리고 div props(`aria-label` 등). `data-slot="resizable-handle"`, grip은 `resizable-handle-grip`.
|
||||
- 접근성(라이브러리): Separator는 `role="separator"`, `aria-orientation`, `aria-valuenow|min|max`(%), `aria-controls`(앞 panel id), `tabindex=0`. 키보드: 가로 배치는 ←/→(5% 단계), 세로 배치는 ↑/↓, Home/End는 최소/최대, collapsible이면 Enter가 접힘을 토글. 의미 있는 이름은 소비자가 `aria-label`로 준다.
|
||||
- Group에는 `role`이 붙지 않는다(라이브러리 구조). 이름은 `aria-label`을 소비자가 준다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `react-resizable-panels` ^4.14.2 (`packages/ui`에 추가, shadcn Resizable의 기반). 사용: Resizable. CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/resizable.css` (`--hm-resizable-*`).
|
||||
|
||||
`divider-width|color`, `hover-color`, `active-color`, `focus-color`, `halo-width`, `grip-width|height|radius|background|border-color|icon-size|icon-color`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`, `--hm-warm-neutral-mid`)만 참조하며 `--hm-separator-*`는 참조하지 않는다. 신규 Primitive와 Semantic 없음. 규칙은 `@layer components`에 있어 소비자 utility가 이긴다.
|
||||
|
||||
## 8. 라이브러리에서 유지한 항목
|
||||
|
||||
drag(pointer)와 최소/최대 clamp, collapsible, keyboard resize, double click 동작, layout callback, ARIA, 커서 처리, panel inline sizing.
|
||||
|
||||
## 9. 소비자 확장성
|
||||
|
||||
- Group 크기: `className`/`style`(Story는 frame의 `height`/`max-width`)로 정한다.
|
||||
- Divider/Grip 색과 크기: `--hm-resizable-*` token을 덮거나 `ResizableHandle className`의 utility를 쓴다.
|
||||
- Panel 내용과 최소 크기: 소비자가 `minSize`를 정한다(**`minSize` 없이 끌면 panel이 0까지 줄어들 수 있다.** Story는 모든 panel에 `minSize`를 둔다).
|
||||
|
||||
## 10. Storybook
|
||||
|
||||
- 대표 Story `All`("Resizable"): Horizontal(list min 15%), Vertical with grip, Horizontal with grip, Min/max(25%–60%), Collapsible panel을 한 페이지에 둔다. 프레임과 panel 내용은 Story 배치이며 Dashboard가 아니다.
|
||||
- verification: `Horizontal`, `Vertical`, `Min Max`.
|
||||
|
||||
## 11. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Dashboard state, 저장, preset은 이후 Layout Pattern에서 조합한다(작업 지시).
|
||||
|
||||
## 12. 미확정 / 보류
|
||||
|
||||
- 수치(1px divider, 12×16 grip, hover/active 색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- **모바일**: 390px에서 가로 분할은 사용 가능하다(panel 107/249px). 하지만 좁은 화면의 가로 분할은 정보 밀도에 부적합할 수 있어, 자동으로 세로로 바꾸는 responsive는 만들지 않았다. 모바일 layout 대체는 Dashboard/Layout 단계에서 검토한다.
|
||||
- 중첩 Group, 3개 이상 panel, Grid(`Grid`/`Cell`/`Gridline`, 2D)는 검증하지 않았다.
|
||||
- 레이아웃 저장(`useDefaultLayout`, localStorage)과 사용자 preference는 만들지 않았다.
|
||||
- 터치 기기에서의 pointer target(`resizeTargetMinimumSize.coarse`)은 라이브러리 기본이다.
|
||||
|
||||
## 13. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/resizable.tsx
|
||||
packages/ui/src/theme/resizable.css
|
||||
packages/ui/package.json (dependency: react-resizable-panels)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/resizable.stories.tsx
|
||||
```
|
||||
|
||||
## 14. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 DOM(Browser pane): Separator `role=separator`, `aria-orientation`(가로 배치는 vertical, 세로 배치는 horizontal), `aria-valuenow`가 30 → 50.6(drag 후), `aria-valuemin`/`max`가 panel의 min/max를 반영(예: list min 15, 다른 panel min 20이면 max 80), `aria-controls`, `tabindex=0`, divider 1px `rgb(222,219,210)`.
|
||||
- pointer drag: divider를 오른쪽으로 끌어 30% → 50.6%(panel 282/275), 왼쪽으로 끝까지 끌어 min 15%(84px)에서 멈춤, 오른쪽 끝까지 끌면 다른 panel에 `minSize`가 없어 0까지 줄어듦(Story에 `minSize`를 추가하는 근거). 세로 분할은 아래로 drag해 40% → 67%.
|
||||
- keyboard: 가로에서 ArrowLeft가 5%씩 줄고, Home이 최소(15), End가 최대, 세로에서는 ArrowUp이 줄고 ArrowRight는 무시, collapsible에서 Home이 0으로 접고 Enter가 이전 크기(30%)로 복원. focus 시 divider와 halo가 `rgb(18,51,40)`.
|
||||
- 레이아웃: Group `flex`, panel 크기는 라이브러리 inline style, grip은 12×16px이고 쌓인 panel 사이에서 90° 회전.
|
||||
- Responsive(390×844): 모든 Group이 356px 안에 들어가고 페이지 가로 overflow 없음, 가로 분할 panel 107/249px.
|
||||
- 미확인: 터치 drag, 중첩/3개 이상 panel, hover 상태(`data-separator="hover"`)의 pointer 시각, 포인터 hit target의 정확한 범위(8px 밖은 잡히지 않음만 확인).
|
||||
@@ -0,0 +1,98 @@
|
||||
# Scroll Area
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Scroll Area 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Scroll Area (Radix `ScrollArea`) |
|
||||
| 대표 Story | `Components/Data & Display/Scroll Area` → `All` (이름 "Scroll Area") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Scroll Area가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix ScrollArea 구조를 유지. scroll 동작(wheel, touch, native keyboard, viewport 측정, thumb drag)은 Radix가 소유하고 scrollbar 모양만 정한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Scrollbar | 두께 10px(`p-px`, 투명 border로 가장자리 여백), 트랙은 투명 |
|
||||
| Thumb | `--hm-sem-stroke-default`, hover/drag 중 `--hm-sem-fg-secondary`, radius 9999px |
|
||||
| 노출 | Radix 기본 `type="hover"`: 마우스가 올라가 있거나 scroll 중에만 scrollbar가 나타난다 |
|
||||
| Orientation | 세로는 기본, 가로는 `<ScrollBar orientation="horizontal" />`를 추가 |
|
||||
| Corner | Radix Corner |
|
||||
| 크기 | 없음. Root에 `className`/`style`로 높이·폭 지정(Story의 크기는 Story 배치) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Scrollbar visible / hidden | Radix `data-state="visible" | "hidden"` |
|
||||
| Thumb hover / active | 색이 한 단계 어두워짐 |
|
||||
| Viewport focus | shadcn ring(`focus-visible:ring-[3px] ring-ring/50`) 유지 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix ScrollArea props 그대로. export: `ScrollArea`, `ScrollBar`.
|
||||
|
||||
- `ScrollArea`: `type`(`hover` 기본 | `auto` | `always` | `scroll`), `scrollHideDelay`, `dir`. Viewport, 세로 ScrollBar, Corner를 내장한다.
|
||||
- `ScrollBar`: `orientation` `"vertical"`(기본) | `"horizontal"`. 가로 scroll이 필요하면 소비자가 직접 추가한다(shadcn 구조).
|
||||
- 출력 `data-slot="scroll-area|scroll-area-viewport|scroll-area-scrollbar|scroll-area-thumb"`, `data-orientation`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/scroll-area.css` (`--hm-scroll-area-*`).
|
||||
|
||||
`scrollbar-size`, `thumb-background`, `thumb-hover-background`, `thumb-radius`. Semantic(`--hm-sem-stroke-default`, `--hm-sem-fg-secondary`)을 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·scroll 동작·scrollbar `type`, viewport의 focus ring, `touch-none select-none` scrollbar, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-border` thumb과 scrollbar 두께 class(`w-2.5`/`h-2.5`)를 Token으로 이전, thumb 색을 베이지 배경에서 보이는 warm neutral로 지정.
|
||||
- 노출 방식은 변경하지 않았다(Radix 기본 hover).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Scroll Area"): Vertical, Horizontal, Both axes, Table-like content를 한 페이지에 둔다.
|
||||
- verification: `Vertical`, `Horizontal`.
|
||||
- 영역 크기는 Story 배치(`style`)이며 Scroll Area의 크기 API가 아니다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 두께 10px, thumb 색, 항상 노출 vs hover 노출은 현재 HM 규격이며 디자인 확정 시 재검토(지금은 Radix 기본 hover).
|
||||
- 터치 기기에서는 Radix가 native scroll을 쓰지만 scrollbar 노출은 `type`에 따른다.
|
||||
- native 키보드 scroll: Viewport는 `tabindex=-1`이라 Tab 순서에 들어가지 않는다(shadcn 구조 그대로). 키보드 사용자를 위한 focus 가능 scroll 영역 정책(예: `tabIndex={0}`)은 사용처가 정해질 때 결정한다.
|
||||
- 가로 scroll이 필요한 경우 `ScrollBar orientation="horizontal"` 추가를 잊기 쉽다(shadcn 구조).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/scroll-area.tsx
|
||||
packages/ui/src/theme/scroll-area.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/scroll-area.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 마우스 wheel로 세로 scroll(`scrollTop` 0 → 300), thumb 위에서 아래로 drag해 `scrollTop` 300 → 456, 가로 wheel로 `scrollLeft` 0 → 300, 마우스를 올리면 scrollbar가 `data-state="visible"`로 나타나고 thumb 7×39, 색 `rgb(154,146,132)`, scrollbar 폭 10px.
|
||||
- 구조: 4개 Specimen에서 세로/가로/양방향 overflow가 각각 의도대로(세로만, 가로만, 둘 다) 생김. Viewport는 `focus()` 가능.
|
||||
- Responsive(390×844): 영역이 고정 크기(280×200 등)로 유지되고 페이지 가로 overflow 없음.
|
||||
- 미확인: 화살표 키 native scroll(도구의 합성 키 입력으로 확인되지 않음), 터치 scroll, 가로 scrollbar thumb drag, corner 시각.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Select
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Select 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-select-trigger` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Select, Radix `Select` |
|
||||
| 대표 Story | `Components/Form/Select` → `All` (이름 "Select") |
|
||||
| 보류 | Item Disabled, 일반 Focus 두께 등 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size/높이(select.md §1.1 D-004, §8), 색·border(§10, §1.3, D-005, M06), indicator glyph(§7, 사용자 제공 Source), Trigger/Content 연결 윤곽(§8).
|
||||
- **ui_viz BASELINE_VALUE**: padding-x 12, gap 12, option 높이 42(§8, §17) — "현재 적용 계약"으로 표기된 값.
|
||||
- **HM 사용자 결정**: Trigger Disabled opacity `0.6`(2026-10-02, §10).
|
||||
- **shadcn/Radix 유지**: 구조·접근성·positioning(§7).
|
||||
- **HM 사용자 결정(2026-10-06)**: Error는 Destructive border, Keyboard Focus는 독립 Focus Semantic ring.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | 외곽 | Trigger 내부 높이 | Trigger 글자 |
|
||||
|---|---|---|---|
|
||||
| `medium` | 34 | 32 | 14 / 20 / 400 |
|
||||
| `large` (기본) | 44 | 42 | 16 / 24 / 400 |
|
||||
|
||||
- 높이는 `min-height = 내부 높이 + 1px border × 2`.
|
||||
- Padding-x 12, gap 12, border 1px `--hm-sem-stroke-input-default`, radius `--hm-radius-input`, face `--hm-sem-bg-surface`.
|
||||
- Placeholder(`[data-placeholder]`): `--hm-sem-fg-placeholder`.
|
||||
- Trigger와 Value는 `min-width: 0`; 긴 placeholder/선택값은 한 줄 ellipsis로 줄고 18px indicator를 밀지 않는다. ui_viz 공개 Story에 없던 현재 HM 복원력 규칙이며 parity 판정 근거로 쓰지 않는다.
|
||||
- Icon: HM glyph, 18×18 `viewBox`, `M5 7 L9 11 L13 7`, stroke 1.50542, 색 `#2d2d2d`(`--hm-select-indicator-color`).
|
||||
- Content: Trigger와 같은 폭(`--radix-select-trigger-width`), border 1px focus 색, `bottom`일 때 아래쪽만 radius.
|
||||
- Option: Trigger의 size를 따른다. min-height 42(두 size 공통, ui_viz §8), padding-x 12, 글자 Medium 14/20/400(Medium Trigger와 동일) · Large 16/24/400, radius 0. ui_viz는 Medium Option 글자 Role을 TBD로 남겼고, 2026-10-06 Trigger 값에 맞추기로 했다(사용자 요청). 높이·폭은 size로 바꾸지 않는다(Content = Trigger 외곽 폭).
|
||||
- size 전달: `SelectTrigger size`가 유일한 지정처다. `Select`가 내부 context로 받아 Portal된 `SelectContent`의 `data-size`로 노출하고, `select.css`가 `[data-slot="select-content"][data-size]`로 Option profile을 정한다. `SelectContent`·`SelectItem`에는 size prop이 없다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | ui_viz Confirmed |
|
||||
| Focus (`:focus`) | border 색 → `--hm-sem-stroke-input-focus`, 두께 1px 유지 | ui_viz Confirmed (1.5px 두께는 ui_viz FOLLOW_UP으로 보류) |
|
||||
| Open (`[data-state="open"]`) | border 색 → focus 색, 위쪽 모서리만 round(리스트와 이어짐) | ui_viz Confirmed |
|
||||
| Selected (Item `[data-state="checked"]`) | Hover와 같은 배경 면(`--hm-sem-surface-highlight`, Accent 45%), border·check icon 없음 | 표현: ui_viz Confirmed / 색: shadcn Token(`accent`) |
|
||||
| Highlighted (Item `focus:`/`data-highlighted`) | Accent 45%(`--hm-sem-surface-highlight`) | 위와 동일 |
|
||||
| Disabled (Trigger `:disabled`) | Default paint 유지, `opacity: 0.6`, `cursor: not-allowed` | opacity: 사용자 결정 |
|
||||
| Disabled (Item `data-disabled`) | shadcn 기본 `opacity-50`, 포인터 차단 | **미검증**(§11) |
|
||||
| Invalid (`aria-invalid="true"`) | border 1px `--hm-sem-action-destructive`, Error ring 없음 | 2026-10-06 사용자 결정 |
|
||||
| Invalid + Keyboard Focus (`:focus-visible`) | Destructive border 유지 + Focus Semantic 3px / 50% outer ring | 위와 동일 |
|
||||
| Invalid + Open | Trigger Destructive border, 연결 seam은 Neutral. Content의 기존 Surface / Focus outer border / Accent 45% Option 유지 | 위와 동일 |
|
||||
| Disabled Invalid | Destructive border + 기존 opacity 0.6, focus 불가 | 위와 동일 |
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
SelectTrigger size:
|
||||
"sm" | "default"
|
||||
→
|
||||
"medium" | "large" 기본 "large"
|
||||
|
||||
SelectContent sideOffset:
|
||||
shadcn 기본 4
|
||||
→
|
||||
기본 -1 (Trigger와 1px border를 공유)
|
||||
|
||||
SelectItem:
|
||||
children 앞의 ItemIndicator(check icon) 제거. children만 ItemText로 렌더.
|
||||
```
|
||||
|
||||
- `Select`, `SelectValue`, `SelectGroup`, `SelectLabel`, `SelectSeparator`, 스크롤 버튼은 shadcn API 그대로.
|
||||
- 출력 속성: `data-slot="select-trigger"`, `data-size`. HM 전용 `error`/`open`/`focused` prop은 없다. 상태는 Radix 속성(`data-state`, `data-placeholder`, `data-highlighted`, `data-side`)과 `:disabled`로 적용한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/select.css` (`--hm-select-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`(radius), `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`.
|
||||
- Select 전용 값: `--hm-select-control-height-{md,lg}`(32/42), `--hm-select-padding-x`, `--hm-select-gap`, `--hm-select-option-height`, `--hm-select-font-size-*`, `--hm-select-disabled-opacity`, `--hm-select-indicator-color`.
|
||||
- indicator는 `--hm-select-indicator-color` → `--hm-sem-palette-indicator`를 참조한다(Default `#2d2d2d`). 신규 Primitive 없음.
|
||||
- Input·Button과 값(42 등)이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이다.
|
||||
- `--hm-select-disabled-opacity: 0.6`은 Select 전용 Component Token이며 Input의 Disabled와 공유하지 않는다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Root/Trigger/Portal/Content/Viewport/Item/ItemText 구조, 키보드 조작·typeahead·focus 관리, Portal, popper positioning(`position="popper"`, `align="start"`), 스크롤 버튼, 열림/닫힘 animation class, `data-slot`, `cn(..., className)`, Trigger의 native `aria-invalid` 전달, Item의 `data-disabled` 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- Size API(`sm|default` → `medium|large`), 기본 `large`, `data-size`.
|
||||
- `sideOffset` 기본 `-1`, Content 폭을 Trigger 폭에 맞춤, Trigger/Content 연결형 윤곽.
|
||||
- chevron 아이콘을 HM glyph(inline SVG)로 교체.
|
||||
- Item check indicator 제거, Selected를 배경 면으로 표현.
|
||||
- shadcn Trigger의 높이·padding·border·focus ring class 제거 → theme CSS가 담당.
|
||||
- `hm-select-trigger` class 제거.
|
||||
- 변경하지 않음: 스크롤 버튼의 lucide chevron(ChevronUp/Down).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Select"): 구역 `default`(Large, size 생략), `selected`, `disabled`, `open`.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Open`, `Selected`, `Disabled`, `Sizes`("Compare Sizes"), `Medium`, `Large`, `LongContent`, `Scroll`, `Playground`(HM 전용).
|
||||
- `Disabled` Story 구성: Trigger 비활성 상태들(placeholder 포함).
|
||||
- `InvalidFocus` verification Story: 앞/뒤 버튼 사이의 Invalid Trigger와 Disabled Invalid Trigger. Tab / Shift+Tab, Enter / Space, Escape 및 focus return을 실제 키보드로 확인한다.
|
||||
- `LongContent`와 `Scroll`은 ui_viz 공개 Story parity가 아니라 좁은 폭·긴 값·Radix scroll 회귀를 확인하는 HM 전용 Story다.
|
||||
- sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**
|
||||
|
||||
- Select Trigger Disabled opacity `0.6` 확정(`select.css`의 `--hm-select-disabled-opacity`).
|
||||
- 같은 날 Input Disabled `0.6`도 확정되었다(input.md 참조). 현재 적용 범위는 Input / Select이며 전체 디자인 시스템 Disabled opacity로 자동 확대하지 않는다.
|
||||
- **Select Item의 Disabled opacity는 이 결정에 포함하지 않는다.** Trigger와 같은 규격이라고 판단하지 않는다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Item Disabled**: 현재 shadcn 기본 `opacity-50`. 사용자 결정·ui_viz 근거 없음. 미검증.
|
||||
- **FOLLOW_UP(ui_viz)**: Focus/Open 상태의 border 1.5px 두께.
|
||||
- **ui_viz 미정의**: Group/Label/Separator 규격, 스크롤 버튼 규격, `item-aligned` position. Error 표현은 2026-10-06 HM 사용자 결정이 우선한다.
|
||||
- **BASELINE_VALUE**: padding-x 12, gap 12, option 42는 "현재 적용 계약"이며 별도 Confirmed 결정은 아니다.
|
||||
- Selected/Highlighted는 현재 공통 Accent 45% 정책을 쓴다. ui_viz Temporary 표기는 도입 당시 근거다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/select.tsx
|
||||
packages/ui/src/theme/select.css
|
||||
src/components/select.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-05: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 generated CSS의 `bg-[var(...)]` 경고와 500kB chunk warning은 유지된다.
|
||||
- Storybook computed style: Medium/Large Trigger 34/44px, padding-x 12px, radius 4px, border 1px, Option 42px·16/24/400, Disabled Trigger opacity `0.6` 확인.
|
||||
- Open: Trigger와 Content 폭 동일, `sideOffset=-1` 접점, Focus/Open `#123328`, Content border/radius/shadow와 Portal listbox 확인.
|
||||
- Interaction: click open, ArrowUp/Down, Home/End, typeahead, Enter select, Escape, focus return, Disabled open 차단, 32개 Option의 wheel/keyboard scroll 확인.
|
||||
- Responsive: 1280/768/390px에서 popup clipping·문서 가로 overflow 없음. 390px의 280px Trigger에서 긴 placeholder/선택값 ellipsis와 indicator 비중첩 확인.
|
||||
- Story: `All`의 공개 구역 4개와 숨김 `LongContent`/`Scroll` verification Story 확인.
|
||||
- 미확인/보류: Item Disabled 시각, collision으로 `side="top"`이 된 연결 윤곽의 별도 디자인(원 ui_viz도 viewport edge flip TBD), 브라우저별 렌더 차이.
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 숨김 `InvalidFocus`에서 Closed Invalid / Tab Focus / Invalid Open / 방향키 강조 / Disabled Invalid 확인. Invalid border computed 1px; Tab 전후 shadow none → 3px Focus ring. Tab은 Disabled를 건너뛰고, Shift+Tab으로 복귀. Enter / Space open, ArrowDown 이동, Escape close 및 Trigger focus return 확인.
|
||||
- Invalid + Open은 Trigger Destructive border와 Neutral seam `rgb(222, 219, 210)`을 동시에 유지. Popup white Surface, 외곽 `rgb(199, 91, 0)`(Focus), Selected / Highlighted `color(srgb 1 0.827451 0.427451 / 0.45)`(Accent) 확인. Popup에 Error 색 없음. 대표 `All` 회귀 확인.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Separator
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Separator 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Separator (Radix `Separator`) |
|
||||
| 대표 Story | `Components/Data & Display/Separator` → `All` (이름 "Separator") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 독립 Separator 규격이 없다(컴포넌트별 divider 값만 있다). 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: Radix Separator의 `orientation`과 `decorative`(기본 true, `role=none`) API를 유지하고 theme CSS로 두께·색을 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Thickness | 1px (`--hm-separator-thickness`) |
|
||||
| Color | `--hm-separator-color` → Semantic `--hm-sem-stroke-input-default` (`#DEDBD2`) |
|
||||
| Horizontal | 높이 1px, 폭 100% |
|
||||
| Vertical | 폭 1px, `align-self: stretch`, `min-height: 1em` (flex row 안에서 사용) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음. `decorative`(기본 true)는 `role="none"`, `decorative={false}`는 `role="separator"`.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Separator props 그대로 (`orientation`: `"horizontal"` 기본 / `"vertical"`, `decorative`: 기본 true). 출력 속성: `data-slot="separator"`, `data-orientation`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/separator.css`: `--hm-separator-thickness`, `--hm-separator-color`. 현재 공통 `--hm-sem-stroke-input-default`를 참조한다. Component Token은 독립적으로 유지한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 접근성(`decorative`, `aria-orientation`), `data-slot`, `cn(..., className)`, `shrink-0`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn의 `bg-border`, `h-px`/`w-px` class를 제거하고 theme CSS가 두께·색을 담당.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `horizontal`, `vertical`, `semantic`(decorative=false). verification: `Horizontal`, `Vertical`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 구분선 전용 Semantic(`stroke.divider` 등) 도입: 미결정. 현재 공통 stroke Semantic과 구분할 필요가 생기면 재검토.
|
||||
- 텍스트가 들어간 구분선("or"), 두께 변형: 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/separator.tsx
|
||||
packages/ui/src/theme/separator.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Separator)
|
||||
src/components/separator.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: horizontal 높이 1px·폭 100%, vertical 폭 1px·높이 24px(flex row stretch), 색 `rgb(222,219,210)`.
|
||||
- a11y: 기본 `role=none`, `decorative={false}`이면 `role=separator` 확인.
|
||||
- 미확인: 사이드바 화면.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Sheet
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Sheet 고유의 API·Token·상태·결정만 기록한다. Dialog 문서(`dialog.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Sheet (Radix `Dialog`) |
|
||||
| 대표 Story | `Components/Overlay/Sheet` → `All` (이름 "Sheet") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Sheet가 없다(Dialog 문서가 "side sheet"를 범위 밖으로 둠). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog에서 검증한 접근성 원칙(Radix modal, `asChild` 처리)은 재사용하고, 면·윤곽·그림자 등 수치는 현재 HM Semantic으로 새로 정했다. `--hm-dialog-*`를 참조하지 않는다.
|
||||
- **구현 방식**: shadcn / Radix Dialog 기반 Sheet 구조와 `side` API 유지, theme CSS로 HM 스타일 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-stroke-input-default`, 페이지를 향한 한 변에만 |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Radius | 없음(viewport edge에 붙음) |
|
||||
| left / right | 폭 `min(400px, 100vw − 48px)`, 높이 100% |
|
||||
| top / bottom | 폭 100%, 높이 내용 기준, 최대 `100dvh − 48px` |
|
||||
| Padding | 24px (Header·Footer·Body 좌우) |
|
||||
| Header | padding `24 (24+36) 16 24` (오른쪽은 Close 버튼 자리), Title↔Description gap 8px |
|
||||
| Title | 20 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary` |
|
||||
| Body | 14 / 20 / 400, 좌우 padding 24px, 유일한 scroll 영역 |
|
||||
| Footer | padding `16 24 24`, gap 8px, 오른쪽 정렬(좁은 화면은 세로, shadcn `flex-col-reverse sm:flex-row`) |
|
||||
| Close(X) | shadcn 기본(ghost `icon-sm`, 오른쪽 위 12px, sr-only "Close") |
|
||||
| Overlay / animation | shadcn 기본(`bg-black/10` + blur, side별 slide) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock |
|
||||
| Focus | 열릴 때 Radix 기본(첫 tabbable), 닫힐 때 trigger로 복귀 |
|
||||
| Long Content | Header·Footer 고정, Body만 세로 scroll |
|
||||
| Form | 소비 조합(Body 안 Label + Input) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Dialog props 그대로. export: `Sheet`, `SheetTrigger`, `SheetClose`, `SheetPortal`, `SheetOverlay`, `SheetContent`, `SheetHeader`, `SheetBody`, `SheetFooter`, `SheetTitle`, `SheetDescription`.
|
||||
|
||||
- `SheetContent`: `side` `"top" | "right" | "bottom" | "left"` (기본 `right`, shadcn API 유지), `showCloseButton`(기본 true). 출력 `data-side`.
|
||||
- `SheetBody`(신규): scroll owner. 없어도 동작하지만 이 경우 Body padding과 내부 scroll이 없다.
|
||||
- `SheetClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙, Button HM 스타일 보존).
|
||||
- 새 prop(`size`, `variant`)은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/sheet.css` (`--hm-sheet-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `shadow`, `width`, `edge-gap`, `padding`, `header-gap`, `footer-gap`, `close-clearance`, `title-*`, `description-*`, `body-font-size`, `body-line-height`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=dialog`, `aria-labelledby/describedby`)·focus trap·initial focus·focus 복귀·Escape·바깥 클릭 닫힘·scroll lock, Portal·Overlay, position(`inset-*`)·side별 slide animation class, `data-slot`, `cn(..., className)`, Close 버튼.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-popover text-popover-foreground shadow-lg gap-4 border-*`, side별 `w-3/4 sm:max-w-sm`·`h-auto`, Header/Footer `p-4` 제거 → theme CSS.
|
||||
- `SheetBody` 추가(scroll owner), `SheetClose asChild` data-slot 처리.
|
||||
- 폭을 `min(400px, 100vw − 48px)`로 지정(좁은 화면에서 Overlay 띠 48px 유지).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Sheet"): 트리거 6개(right, left, top, bottom, 폼, 긴 내용)가 각각 실제 Sheet를 연다.
|
||||
- verification: `Right`, `Left`, `Top`, `Bottom`, `Form`, `Long Content`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(400px 폭, 24px padding, 그림자, 48px edge gap)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay 색·blur, Motion은 정해진 규칙이 없어 shadcn 기본 유지.
|
||||
- Header/Footer divider 없음(미정의). Close 버튼 규격은 shadcn 기본.
|
||||
- top/bottom side 사용처는 Drawer와 겹친다. 둘의 역할 경계(Drawer = 드래그로 닫는 하단 panel)는 사용 가이드로 정리 예정.
|
||||
- 소비 화면이 쓰는 공통 header/footer 높이 대응은 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/sheet.tsx
|
||||
packages/ui/src/theme/sheet.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/sheet.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기(focus가 "취소"로 이동, `role=dialog`), Escape 닫힘과 trigger focus 복귀, 바깥(Overlay) 클릭 닫힘과 focus 복귀, X 닫기, 네 방향 위치(right 우측 400px, left 좌측 0, top/bottom 전체 폭), 긴 내용에서 Body만 scroll하고 Footer가 화면 안.
|
||||
- 390×844: right Sheet 폭 342px(left 82px), 가로 overflow 없음.
|
||||
- computed style: white, border-left `rgb(222,219,210)`, shadow `0 0 12px .2`.
|
||||
- 미확인: focus trap의 Tab 순환(코드 경로는 Radix 기본), side별 닫힘 animation 시각, top/bottom의 모바일 높이 한계.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Skeleton
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Skeleton 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Skeleton (`div` + `animate-pulse`, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Skeleton` → `All` (이름 "Skeleton") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Skeleton이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn Skeleton을 유지. loading 로직이 없는 시각 placeholder primitive이며 크기·모양은 `className`으로 정한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 배경 | `--hm-sem-palette-warm-neutral-light` |
|
||||
| Radius | `--hm-radius-control-md` (4px). 원형은 `className="rounded-full"` |
|
||||
| Animation | shadcn `animate-pulse`(2s). `prefers-reduced-motion: reduce`에서는 animation 없음 |
|
||||
| 크기 | 없음. `className`으로 `h-*`, `w-*`, `size-*` 지정 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음(placeholder). 배경과 radius는 `@layer components`에 있어 `className`의 utility(`rounded-full`, `bg-*`)가 이긴다. 다른 컴포넌트의 theme CSS(레이어 없음)와 달리 Skeleton은 모양을 소비자가 정하는 것이 역할이기 때문이다.
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Skeleton`. `div` props 그대로. 출력 `data-slot="skeleton"`.
|
||||
|
||||
- `width`/`height`/`variant` prop은 만들지 않았다. Text line, Avatar, Card 등은 Story처럼 `className`으로 조합한다.
|
||||
- 접근성: placeholder 자체에는 ARIA를 두지 않는다. loading 영역의 `aria-busy` 등은 소비자 책임.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/skeleton.css` (`--hm-skeleton-*`).
|
||||
|
||||
`background`, `radius`. Theme palette(`--hm-sem-palette-warm-neutral-light`)와 공통 control radius만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`div` 구조, `animate-pulse`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-muted rounded-md`(또는 `bg-accent`) class를 제거하고 Token으로 연결(배경 `--hm-warm-neutral-light`, radius 4px).
|
||||
- reduced-motion에서 pulse를 끈다(shadcn 기본에는 대응이 없다). 새 Motion token은 만들지 않았다.
|
||||
- 배경·radius 규칙을 `@layer components`에 둔 것은 이 컴포넌트만의 결정이다(§4).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Skeleton"): Text(3줄), Avatar + text, Card/list loading(Card 안 조합)을 한 페이지에 둔다. 데이터 fetch는 없다.
|
||||
- verification: `Text`, `Card List`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 배경색과 radius는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Pulse 속도·형태(shimmer 등)는 Motion 규칙이 정해질 때 다룬다.
|
||||
- Text line 높이 등 사전 정의된 크기 preset은 만들지 않았다.
|
||||
- 전체 loading 영역의 ARIA(`aria-busy`, live region) 가이드는 Data Table Pattern 등 조합 단계에서 정한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/skeleton.tsx
|
||||
packages/ui/src/theme/skeleton.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/skeleton.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더(Browser pane): 배경 `rgb(224,218,206)`, radius 4px, animation `pulse 2s`. `className="size-10 rounded-full"`이 radius override를 받아 원형으로 표시(처음에는 unlayered CSS가 `rounded-full`을 덮는 문제가 있어 `@layer components`로 수정).
|
||||
- Responsive(390×844): Text, Avatar+text, Card 구성이 부모 폭 안에 들어가고 가로 overflow 없음.
|
||||
- 미확인: 실제 `prefers-reduced-motion` 환경에서의 animation 정지(CSS만 작성).
|
||||
@@ -0,0 +1,116 @@
|
||||
# Slider
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Slider 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-06 |
|
||||
| 기반 | shadcn/ui Slider (Radix `Slider`) |
|
||||
| 대표 Story | `Components/Form/Slider` → `All` (이름 "Slider") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Slider가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Slider 구조(Root, Track, Range, Thumb)를 유지하고 theme CSS로 연결. Slider는 값 입력 컨트롤이며 Toggle/Button 계열과 역할이 다르다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 두께 6px, radius 9999px, 배경 `--hm-sem-palette-warm-neutral-light` |
|
||||
| Range | `--hm-sem-state-active` |
|
||||
| Thumb | 18px 원, 배경 `--hm-sem-bg-surface`, 2px `--hm-sem-state-active` border |
|
||||
| Thumb halo | Hover 4px / Drag(`:active`) 6px Primary 20% 후광. 키보드 Focus는 같은 크기의 Focus Semantic 20% 후광, 커서 grab/grabbing 유지 |
|
||||
| 폭/높이 | Root에 `className`. 기본은 부모 폭 100%, vertical은 높이 100%·최소 176px(shadcn 기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 후광 없음, 커서 grab |
|
||||
| Hover | 4px `--hm-sem-action-primary` 20% 후광 |
|
||||
| Focus (`:focus-visible`) | 4px `--hm-sem-stroke-input-focus` 20% 후광 |
|
||||
| Drag (`:active`) | 6px Primary 20% 후광, 커서 grabbing |
|
||||
| Focus + Hover | Focus 색 우선, 4px Focus 20% 후광 |
|
||||
| Focus + Drag | Focus 색 우선, Drag의 6px 크기와 grabbing 커서 유지 |
|
||||
| Disabled (`data-disabled`) | Root `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), 후광 없음, 커서 not-allowed, thumb이 focus를 받지 않음 |
|
||||
| Orientation | `data-orientation` horizontal / vertical |
|
||||
|
||||
우선순위: Disabled > Focus 색 > Hover/Drag interaction 색. Drag는 halo 크기만 6px로 확대한다. Radix의 pointer focus 정책은 유지한다(pointer로 thumb을 잡으면 `focusVisible: false`로 focus하므로 일반 pointer drag는 Primary halo). Focus + Drag 규칙은 실제로 `:focus-visible`과 `:active`가 겹칠 때 적용하며, interaction 규칙이 Focus 표시를 덮지 않는다.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Slider props 그대로. export: `Slider`.
|
||||
|
||||
- `value`/`defaultValue`/`onValueChange`/`onValueCommit`, `min`/`max`/`step`, `minStepsBetweenThumbs`, `orientation`, `dir`, `inverted`, `disabled`, `name`.
|
||||
- 값이 배열이면 항목 수만큼 thumb을 렌더한다(두 개면 range). Range용 별도 컴포넌트는 없다.
|
||||
- `value`와 `defaultValue`를 모두 생략하면 초기값은 `[min]`이고 thumb은 하나다(`min` 기본값 0). 숨겨진 추가 thumb이나 빈 FormData 항목을 만들지 않는다.
|
||||
- Uncontrolled의 `defaultValue`는 마운트 시 초기값이다. 이후 `defaultValue` 배열이나 항목 수를 바꿔도 내부 값과 thumb 수는 초기 배열을 유지한다. 초기화가 필요하면 새 `key`로 재마운트하고, 동적 값 변경에는 Controlled `value`를 사용한다. Controlled/Uncontrolled 모드는 컴포넌트 수명 동안 유지한다.
|
||||
- Form 안에서 `name`을 지정하면 Enabled Slider는 현재 값을 제출한다(단일값은 `name`, 범위값은 `name[]`으로 여러 항목). `form="id"`로 외부 Form을 연결할 수도 있다.
|
||||
- Disabled Slider는 FormData와 native Form Submit에서 제외한다. 설치된 Radix Slider 1.4.7의 내부 input에 `disabled`가 전달되지 않아 HM은 disabled일 때 Radix에 넘기는 `name`을 생략한다. 다시 Enabled로 바꾸면 기존 `name`으로 현재 값 제출을 재개한다. Controlled/Uncontrolled에 동일하게 적용하며 값·callback·키보드·ARIA 계약은 유지한다.
|
||||
- **`aria-label`**: `role="slider"`는 Root가 아니라 Thumb이므로 Slider가 `aria-label`을 모든 thumb에 전달한다(thumb이 둘 이상이면 `"label 1/2"`, `"label 2/2"`). `aria-label`이 없으면 Radix 기본 thumb 이름(Minimum/Maximum)이 남는다.
|
||||
- `variant`, `status`, `appearance` 같은 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/slider.css` (`--hm-slider-*`).
|
||||
|
||||
`track-size`, `track-background`, `range-background`, `thumb-size`, `thumb-background`, `thumb-border-width|color`, `thumb-halo-color|size|size-active`, `thumb-focus-halo-color`, `disabled-opacity`. Interaction halo는 Primary, Focus halo는 기존 Focus Semantic을 참조한다. 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음. Track/Range/Thumb은 내부 부품이라 theme CSS(레이어 밖)로 정하고, Slider Root의 폭·높이는 `className` 영역이다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·동작(pointer drag, track 클릭 이동, 키보드, thumb 정렬/교차 방지), ARIA(`role=slider`, `aria-valuemin|max|now`, `aria-orientation`), 배열 value에서 thumb 수를 계산하는 shadcn 로직, `touch-none select-none`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-muted`, `h-1`, `bg-primary`, thumb `size-3 border-ring ring-*` class를 제거하고 Token으로 이전. thumb을 12px에서 18px로 키웠다.
|
||||
- `aria-label`을 thumb으로 전달(shadcn은 Root에만 붙어 thumb에 이름이 없었다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Slider"): Single(값 label), Range(두 thumb, `minStepsBetweenThumbs` 5), Step(25), Disabled, Vertical을 한 페이지에 둔다. 값 label은 Story 조합(local state)이다.
|
||||
- verification: `Single`, `Range`, `Disabled`, `Focus`(실제 thumb focus). 기본 Sidebar에서 숨긴다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(6px track, 18px thumb, halo 4/6px)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 값 tooltip/label 표시, tick mark, 범위 밖 구간 표현은 정의하지 않았다.
|
||||
- 상태색 range(예: 경고)는 HM 상태색 확정 후.
|
||||
- hit area: thumb 18px이 터치 타깃(44px 권장)보다 작다. 터치 기기 정책은 미정.
|
||||
- 대표 Story의 form 예시는 만들지 않았다. 기본값 및 Enabled/Disabled 제출 계약은 §5를 따른다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/slider.tsx
|
||||
packages/ui/src/theme/slider.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/slider.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 기능 개선: 프로젝트 외부 UI 복사본에서 타입 검사와 패키지 빌드 통과. 실제 프로젝트 Slider 소스와 복사본의 SHA-256이 일치하며, 브라우저는 해당 패키지 빌드 결과를 사용했다.
|
||||
- 브라우저 Form 검증: 기본값 생략/`min=10`은 thumb 하나·정상 `aria-valuenow`·빈 제출 항목 없음. 명시적 단일/범위, Controlled 값·배열 길이 변경, Uncontrolled `defaultValue` 배열 길이 변경 시 초기 thumb 수 유지 확인. Controlled/Uncontrolled 단일·범위의 Disabled → Enabled → Disabled 전환에서 FormData와 실제 native POST 제출 결과 정상. 키보드·callback·ARIA와 외부 `form` 연결의 FormData 확인.
|
||||
- 디자인 보존: Slider CSS·Theme JSON·대표 Story 변경 없음. 수정 전후 computed style에서 thumb 18px/2px border, track 6px, Disabled opacity 0.6, Hover/Focus 4px 후광 일치. 검증 예제와 POST 수집 서버는 외부 임시 환경에서만 실행했다.
|
||||
- 2026-10-06 Focus 정합: `typecheck`, `build`, `build-storybook`, `git diff --check` 통과. 기존 문서 예시 CSS(`bg-[var(...)]`)와 Storybook chunk 크기 경고 유지.
|
||||
- Theme Designer에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`를 분리 적용한 뒤 **일반 verification Story**의 computed style로 확인: Range·Thumb border `rgb(0,127,115)`, Hover/Drag는 Primary 20% / 4·6px, Focus/Focus + Hover는 Focus 20% / 4px, Focus + Drag는 Focus 20% / 6px, Default/Disabled는 shadow 없음. Hover/Drag 중첩은 기존 Storybook pseudo-state mirror로 고정하고 Focus는 실제 DOM focus다.
|
||||
- 각 역할을 단독 override해도 Primary는 interaction halo, Active는 Range·Thumb border, Focus는 Focus halo만 변경하며 Accent는 Slider에 영향 없음. 실제 ArrowRight로 값·ARIA·label 40 → 41 확인. Focus Semantic 이름·값·Runtime/JSON 계약 변경 없음.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 DOM(Browser pane): thumb `role="slider"`, `aria-valuemin=0`, `aria-valuemax=100`, `aria-valuenow`가 실제 값과 일치, vertical은 `aria-orientation=vertical`, disabled thumb은 focus 불가. `aria-label`이 단일 thumb에 `볼륨`, range에 `가격 범위 1/2`·`2/2`로 반영됨.
|
||||
- keyboard: ArrowRight 40 → 41, ArrowLeft → 40, End 100, Home 0, step 25에서 ArrowRight 50 → 75, vertical ArrowUp 30 → 31. 값 label과 ARIA가 함께 변함.
|
||||
- pointer: thumb을 오른쪽으로 drag해 40 → 79(range 폭 382/484), track 클릭으로 23, 왼쪽 경계 밖으로 drag해 0에 고정. Range에서 첫 thumb을 두 번째 thumb 너머로 drag해도 값이 정렬 상태(75, 100)로 유지됨.
|
||||
- computed style: track 6px `rgb(224,218,206)`, range `rgb(18,51,40)`, thumb 18px·2px border·흰 면, disabled Root opacity 0.6.
|
||||
- Responsive(390×844): Slider 폭 358px(좌우 16px), thumb이 잘리지 않고 페이지 가로 overflow 없음.
|
||||
- 미확인: PageUp/PageDown, 터치 drag, `inverted`/RTL, thumb halo의 시각(hover/active 상태 pointer 시각).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Sonner (Toast)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Toast 고유의 API·Token·상태·결정만 기록한다. public export가 `Toaster` / `toast`이고 shadcn 컴포넌트명이 Sonner이므로 문서명은 `sonner.md`로 하며 `toast.md`는 만들지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Sonner (`sonner` 2.0.8) |
|
||||
| 대표 Story | `Components/Feedback/Sonner` → `All` (이름 "Toast") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 Toast 규격이 없고 `color.status.success|warning|information`은 TBD다. 새 Status Palette를 만들지 않았다.
|
||||
- **현재 HM 규격**: surface·text·radius·shadow는 현재 HM Semantic(surface, fg, stroke, destructive)에서 정했다.
|
||||
- **구현 방식**: Sonner가 queue·stack·timeout·hover pause·swipe·promise·`aria-live`를 소유한다. 오래된 Radix Toast는 만들지 않는다(Toast는 Sonner 하나).
|
||||
|
||||
## 3. Alert / Dialog와 차이
|
||||
|
||||
| | 성격 | 닫힘 | focus |
|
||||
|---|---|---|---|
|
||||
| Alert | 화면 안에 지속적으로 존재하는 in-page feedback | 사용자가 처리하거나 화면이 바뀔 때까지 | 흐름을 막지 않음 |
|
||||
| Toast | 일시적인 global notification (저장 완료, 오류, 비동기 진행) | timeout 또는 닫기 / action | 흐름을 막지 않음 |
|
||||
| Alert Dialog | 사용자의 응답이 필요한 modal 확인 | 응답으로만 | focus trap |
|
||||
|
||||
Toast는 anchored floating panel이 아니라 notification surface이므로 Floating Surface Semantic(`--hm-sem-floating-*`)을 쓰지 않고 `--hm-toast-*`를 별도로 둔다.
|
||||
|
||||
## 4. 기본 규격 / 상태 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Width | 356px (Sonner 기본 유지), 좁은 화면(≤600px)은 Sonner가 좌우 16px offset으로 줄임 |
|
||||
| Surface | `--hm-sem-bg-surface`, 1px `--hm-sem-stroke-input-default`, radius `--hm-radius-container`(4px), 그림자 `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding / gap | 16px / 12px |
|
||||
| Title | 14 / 20 / 500, Description 13 / 18 / 400 `--hm-sem-fg-secondary` |
|
||||
| Icon | 16px, shadcn(lucide) `CircleCheck`·`Info`·`TriangleAlert`·`OctagonX`·`LoaderCircle(spin)` |
|
||||
| Default / Success / Info / Warning / Loading | 중립 면. 의미는 icon과 문구가 전달한다 |
|
||||
| Error (`data-type="error"`) | 윤곽·icon·title `--hm-sem-action-destructive` (Alert destructive와 같은 방침) |
|
||||
| Action | 높이 28px, radius 4px, 12/500, `--hm-sem-action-primary` 면 + `--hm-sem-on-action-primary` 글자 |
|
||||
| Cancel | `--hm-sem-bg-subtle` 면 + `--hm-sem-fg-secondary` 글자 |
|
||||
| Close button | Sonner `closeButton` 옵션. 면·윤곽은 Toast와 같고 글자 `--hm-sem-fg-secondary` |
|
||||
|
||||
Action은 HM `Button`을 넣지 않고 Sonner의 `action` / `cancel` 버튼을 Toast 전용 token으로 스타일한다(Button token 상속 없음).
|
||||
|
||||
## 5. API
|
||||
|
||||
```tsx
|
||||
import { Toaster, toast } from "@hm-design/ui"
|
||||
// 앱 root에 한 번: <Toaster />
|
||||
toast("저장했습니다")
|
||||
toast.success("..."); toast.error("..."); toast.warning("..."); toast.info("..."); toast.loading("...")
|
||||
toast.promise(promise, { loading, success, error })
|
||||
toast("삭제했습니다", { action: { label: "실행 취소", onClick }, cancel: { label, onClick }, closeButton: true, duration })
|
||||
toast.dismiss()
|
||||
```
|
||||
|
||||
- `Toaster`는 Sonner `Toaster`에 shadcn icon set만 주입한 것이다. `position`, `closeButton`, `expand`, `richColors`, `duration`, `offset` 등 Sonner props를 그대로 받는다. HM 전용 prop과 position system은 없다.
|
||||
- `toast`는 `sonner`의 `toast`를 re-export한다. 소비자는 `sonner`를 직접 import하거나 설치할 필요가 없다. 타입 `ToasterProps`, `ExternalToast`도 export한다.
|
||||
- Light/Dark 전환(`next-themes`)은 쓰지 않는다. `theme`은 Sonner 기본값이다.
|
||||
- `richColors`를 켜면 Sonner의 자체 상태 색이 적용되어 HM 규격 밖이 되므로 쓰지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/sonner.css` (`--hm-toast-*`).
|
||||
|
||||
`width`, `background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `gap`, `font-family`, `title-*`, `description-*`, `icon-size`, `icon-color`, `error-border-color`, `error-text`, `action-*`, `cancel-*`, `close-*`. Semantic 또는 Theme palette만 참조한다. 신규 Primitive·Semantic 없음.
|
||||
|
||||
Sonner 변수(`--width`, `--border-radius`, `--normal-bg|border|text`)는 `[data-sonner-toaster]`에서 이 token으로 연결한다. 상태 hook은 Sonner의 자체 속성(`[data-sonner-toast]`, `[data-type]`, `[data-button]`, `[data-cancel]`, `[data-close-button]`)이다. Sonner CSS가 런타임에 주입되므로 selector에 속성을 겹쳐 specificity를 확보했다.
|
||||
|
||||
**Notification Semantic 후보(미승격)**: Toast 표면 값이 Floating surface와 현재 같지만 역할이 달라 별도 Semantic으로 승격하지 않았다. Status(success·warning·information) 색이 확정되면 `--hm-sem-status-*` 후보와 함께 검토한다.
|
||||
|
||||
## 7. shadcn / Sonner에서 유지한 항목
|
||||
|
||||
queue·stack·expand·hover pause·swipe·timeout·promise lifecycle, `section[aria-live="polite"]`(label `Notifications alt+T`, `aria-relevant`), 단축키(Alt+T로 focus), toast의 `tabindex=0`, 기본 position(`bottom-right`), 모바일 offset·폭 처리, shadcn icon set.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn Sonner의 `next-themes`·`--popover` 계열 inline style을 제거하고 theme CSS로 대체.
|
||||
- Sonner 기본 font(`ui-sans-serif`)를 HM font family로, 기본 radius 8px를 4px로 변경.
|
||||
- action 버튼의 Sonner 기본 반전색(`--normal-bg`/`--normal-text`)을 HM primary로 변경.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Sonner"): 버튼으로 실제 Toast를 생성한다 — Types(Default·Success·Error·Warning·Info·Loading), Action / Action + Cancel / Close button, Promise(→ Success / → Error, 로컬 timeout), Stack ×3 / Long message / Dismiss all.
|
||||
- `Toaster`는 `.storybook/preview.tsx`에 한 번만 둔다(Story별 중복 없음). 정적 Toast specimen은 없다.
|
||||
- Root Demo에는 추가하지 않았다(필수 아님).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: Toast는 Sonner 하나로 통일한다(Radix Toast 미사용). Floating Semantic과 분리된 Notification UI로 설계한다. 새 Status Palette를 임의로 만들지 않는다(작업 지시).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- Success / Warning / Info의 상태 색은 ui-viz에서 TBD라 중립 면으로 두었다. 확정되면 `--hm-toast-<status>-*`를 추가한다.
|
||||
- 수치(356px, 16px padding, 28px action 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 한국어 문장이 좁은 폭에서 어절 중간에서 줄바꿈될 수 있다(Sonner 기본 `word-break`). 규격이 정해지면 `keep-all` 검토.
|
||||
- `position` 비교, 키보드 focus 이동(Alt+T → Tab), 스크린 리더 낭독, swipe 닫기는 확인하지 못했다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/sonner.tsx
|
||||
packages/ui/src/theme/sonner.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
packages/ui/package.json (dependency: sonner)
|
||||
.storybook/preview.tsx (Toaster 1회)
|
||||
src/components/sonner.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 interaction(Browser pane): Default·Success·Error·Warning·Info·Loading 생성, Stack ×3(겹침과 hover 펼침), Action + Cancel 표시와 Action 클릭 시 닫힘, Close button 표시, Dismiss all, Promise → Error(loading → error 전환, 윤곽·icon·title destructive), timeout 자동 닫힘.
|
||||
- DOM: `section[aria-live=polite][aria-relevant="additions text"]`, label `Notifications alt+T`, toast `tabindex=0`.
|
||||
- computed style: surface white, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`, width 356px, padding 16px, title 14px, action `rgb(18,51,40)`·28px.
|
||||
- 390px: 폭 358px(좌우 16px), 가로 스크롤 없음, 긴 title·description·action·close button 모두 폭 안에서 줄바꿈.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Switch
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Switch 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Switch (Radix `Switch`) |
|
||||
| 대표 Story | `Components/Form/Switch` → `All` (이름 "Switch") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Switch 규격이 없다(후보 목록뿐). 참고할 수치가 없어 현재 HM Semantic으로 새로 정했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: Radix Switch(`role=switch`, `data-state`, Space/Enter, 폼 동작)와 shadcn의 transition(`transition-all`, 썸 `transition-transform`)을 유지하고 theme CSS로 HM 스타일을 연결. 새 motion은 정의하지 않았다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 36 × 20px, pill radius |
|
||||
| Thumb | 16px 원, `--hm-sem-bg-surface`, 좌우 offset 2px (On: translate `36 − 16 − 2`) |
|
||||
| Off 색 | track `--hm-sem-stroke-default` |
|
||||
| On 색 | track `--hm-sem-state-active` |
|
||||
| Label | `<Label htmlFor>` 조합 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Off / On | 위 기본 규격 (`data-state="unchecked"` / `"checked"`) |
|
||||
| Hover | Black 20% overlay (`--hm-color-action-overlay-hover`) |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed` |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Switch props 그대로 (`checked`/`defaultChecked`/`onCheckedChange`, `disabled`, `required`, `name`, `value`). 추가 prop 없음. 출력 속성: `data-slot="switch"`, `"switch-thumb"`, Radix `data-state`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/switch.css` (`--hm-switch-*`): `width`, `height`, `thumb-size`, `thumb-offset`, `off-background`, `on-background`, `thumb-color`, `hover-overlay`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·키보드, `data-slot`, `cn(..., className)`, shadcn transition, focus ring, disabled 커서.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn의 `h-[1.15rem] w-8`, `border`, `shadow-xs`, `dark:` class 제거 → theme CSS. Off 색을 `input`(옅음) 대신 stroke 색으로, Disabled opacity 50 → 60.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `off`, `on`, `interactive`(controlled), `hover`, `focus`, `disabled`. Focus는 `play`로 실제 focus. verification: `Off`, `On`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- size 변형(sm/md), 라벨 위치 규칙: 미결정.
|
||||
- 수치(36×20, 썸 16)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- motion은 shadcn 기본 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/switch.tsx
|
||||
packages/ui/src/theme/switch.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Switch)
|
||||
src/components/switch.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: track 36×20, Off `rgb(154,146,132)`, On `rgb(18,51,40)`, 썸 16px White, translate 2px / 18px, Disabled opacity 0.6.
|
||||
- interaction: controlled click으로 `data-state`·`aria-checked` 토글 확인.
|
||||
- 미확인: 실제 pointer hover·focus ring 시각, transition 실제 재생, 사이드바 화면.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Table
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Table 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Table (semantic HTML table, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Table` → `All` (이름 "Table") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격(`docs/components/table.md`)을 참고했다: 연한 녹색 header 띠, 1px 행 구분선, 셀 padding 14/12, 기본 최소 폭 520px, radius 없음, 행 hover는 면만 변경, header 13/20/700, 셀 14/20/400, 숫자 tabular numerals. 값은 현재 HM Token(Primitive 조합)으로 다시 연결했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. ui-viz에 없던 selected row와 footer는 새로 정했다.
|
||||
- **구현 방식**: shadcn Table 구조(container div + table 요소들)를 유지하고 theme CSS로 연결. **정적 표시 primitive**이며 이후 Data Table Pattern(정렬·필터·선택 state·페이지·열 표시)의 기반이다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Table | min-width 520px(기본값, 덮어쓸 수 있음), 폭 100%, 배경 `--hm-sem-bg-surface`, 글자 `--hm-sem-fg-primary`, radius 없음, `border-collapse: collapse` |
|
||||
| Header cell | 13 / 20 / 700, 글자 `--hm-sem-palette-green-light-1`, 배경 흰색에 `--hm-sem-palette-green-tint` 30% 섞은 연한 면, 위 1px `--hm-sem-palette-green-light-2`, 아래 1px 같은 색 20% |
|
||||
| Cell | 14 / 20 / 400, padding `14px 12px`, 위 1px `--hm-sem-stroke-input-default` 구분선(첫 body 행은 header 경계가 대신함), `tabular-nums` |
|
||||
| Footer | 배경 `--hm-sem-bg-subtle`, weight 500, 위 1px 구분선 |
|
||||
| Caption | 12 / 18, `--hm-sem-fg-secondary`, 왼쪽 정렬, 표 아래(caption-bottom), 위 간격 12px |
|
||||
| 긴 텍스트 | Cell은 줄바꿈(`overflow-wrap: anywhere`), Header는 `nowrap` |
|
||||
| 숫자 열 | 소비자가 `className="text-right"` 지정 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Row Hover | body 행 배경 `--accent` 45%(`color-mix`, 면만 변경, header 제외) |
|
||||
| Selected (`data-state="selected"`) | 행 배경 `--accent` 45%. 소비자가 속성을 붙인다 |
|
||||
| Selected + Hover | `--accent` 45% 유지(농도 변화 없음) |
|
||||
| Focus / Disabled / Loading | Table 자체에는 없음(셀 안 content가 각자 가진다) |
|
||||
|
||||
## 5. API
|
||||
|
||||
shadcn 구조 그대로. export: `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableHead`, `TableRow`, `TableCell`, `TableCaption`.
|
||||
|
||||
- `Table`: `table` props. `div[data-slot=table-container]`(overflow-x auto)로 감싼다.
|
||||
- `TableRow`: `data-state="selected"`를 지원하는 스타일(shadcn 규칙). 선택 state 관리는 소비자.
|
||||
- `density`, `variant`, `striped`, `interactive` 같은 prop은 만들지 않았다. 정렬·필터·선택·column visibility·페이지·resize·server loading은 넣지 않았다(Data Table Pattern에서 조합).
|
||||
- Checkbox 등 셀 안 요소는 소비 조합이며 Table은 runtime 의존이 없다. `[role=checkbox]` 포함 셀은 오른쪽 padding을 줄이고 checkbox를 2px 내리는 shadcn 규칙만 유지.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/table.css` (`--hm-table-*`).
|
||||
|
||||
`min-width`, `background`, `text`, `font-size|line-height|font-weight`, `cell-padding-y|x`, `divider-width|color`, `header-background|text|font-size|font-weight|border-top|border-bottom`, `row-hover-background`, `row-selected-background`, `footer-background|font-weight`, `caption-*`. Semantic과 Theme palette(`--hm-sem-palette-green-*`, `--hm-sem-stroke-input-default`, `--hm-sem-palette-white`)를 참조하고 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음(ui-viz의 `#EDF4F1`은 기존 Primitive의 `color-mix`로 표현). row hover / selected는 shadcn `--accent`의 `color-mix` 45%(Select·Menu·Pagination hover와 같은 Soft Surface 규칙, 공통 `--hm-sem-surface-highlight`를 Component Token이 참조).
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
container(`overflow-x-auto`)와 table 구조, `caption-bottom`, Head/Cell의 checkbox 보정 class, `data-slot`, `cn(..., className)`, `data-state="selected"` 규칙.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 `text-sm`, `h-10 px-2 font-medium`, `p-2`, `border-b`, `hover:bg-muted/50`, `bg-muted/50`, Caption `mt-4 text-muted-foreground` class를 제거하고 theme CSS로 이전.
|
||||
- Cell의 `whitespace-nowrap`을 제거해 긴 텍스트가 줄바꿈되게 했다(Header는 nowrap 유지).
|
||||
- Header 띠 스타일과 숫자 `tabular-nums`, 최소 폭 520px은 ui-viz를 참고한 신규 규격.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Table"): Basic(Badge, 숫자 열, footer, caption), Selected row(Checkbox, local state), Long text, Horizontal overflow(1100px 표)를 한 페이지에 둔다.
|
||||
- verification: `Basic`, `Selected`, `Long Text`, `Horizontal Overflow`.
|
||||
- Checkbox·Badge는 Story 조합이다. Story의 폭 wrapper에는 `minWidth: 0`이 필요하다(grid item의 `min-width:auto`가 Table 최소 폭만큼 부모를 넓힘).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
- 2026-10-06 (컬러 역할 정리): 행은 넓은 Soft Surface이므로 Hover·Selected·Selected+Hover 모두 `--accent` 45%를 쓴다(이전 `--hm-green-tint` 18% / 40%). Active State Semantic(Light 2)은 쓰지 않는다. 선택 구분은 면 색이 아니라 행 안의 Checkbox(Active Semantic)가 담당한다. `--accent` 값 자체는 바꾸지 않았다. Table row highlight surface는 Select Highlight / Selected와 동일한 Accent 45% 정책을 사용한다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(520px, 14/12 padding, header 띠 색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- zebra, sticky header, 정렬/필터/선택/페이지, column resize, empty/loading 표현: 이후 Data Table Pattern.
|
||||
- 모바일: Table을 카드로 자동 변환하지 않는다. container 가로 scroll만 제공하며 모바일용 Data Table 패턴은 Layout/Data Table 단계에서 정한다.
|
||||
- header 수치 13px는 ui-viz 참고값이며 공통 typography role과 연결하지 않았다.
|
||||
- 말줄임(ellipsis) 셀 규칙 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/table.tsx
|
||||
packages/ui/src/theme/table.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/table.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 구조/스타일(Browser pane): `th`/`td` 구조, header 13/700 `rgb(32,82,65)`, 위 border `rgb(38,102,81)`, cell 14/20·padding 14/12, 표 폭 100%·최소 520px, caption bottom·왼쪽 정렬.
|
||||
- 선택 행: Checkbox Space로 선택하면 해당 행이 `data-state="selected"`가 되고 header Checkbox가 `mixed`, header Checkbox 클릭으로 전체 선택. Tab이 행 Checkbox로 이동.
|
||||
- Responsive(390×844): container 폭 358px에 표 520px(1100px 표)이 container 안에서 가로 scroll, 페이지(`scrollWidth` 390) 가로 overflow 없음, 긴 텍스트는 셀 안에서 줄바꿈(89px 행).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): header·cell padding/typography/색, divider, 텍스트 행 49px, 실제 pointer hover 면 `rgba(189,223,212,.18)` 일치. header/첫 행 경계는 ui_viz가 td 위 선을 두고 HM은 없애지만 border-collapse에서 header 하단선이 이기므로 렌더 픽셀이 같음(197,215,208 / 196,216,209). header 면 ±2 차이는 §6 `color-mix` 근사. 390px에서 ui_viz Story는 페이지가 544px로 넘치고 HM은 container scroll만 생김.
|
||||
- 2026-10-06 Accent 45% 전환(실제 pointer hover): Hover·Selected·Selected+Hover 모두 `color(srgb 0.741 0.875 0.831 / 0.45)`(= `#bddfd4` 45%). Data Table 선택 행 동일 + 선택 Checkbox `rgb(38,102,81)` + focus ring 기존 그대로. Dialog Wide Content: header·cell typography·padding·행 높이 49px 유지, hover 동일 값, 좁은 Dialog에서 container만 가로 scroll(1600 / 750).
|
||||
- 미확인: 가로 scroll을 사용자가 끝까지 끌었을 때의 clipping, Dropdown Menu·Pagination 조합(Story에 없음).
|
||||
@@ -0,0 +1,218 @@
|
||||
# Tabs
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tabs 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 기본 사용: Primary (2026-10-08)
|
||||
|
||||
앱 진입점에서 `@hm-design/ui/theme.css`를 로드한다. `TabsList`의 기본값은 `fill=true`, `panelSurface="content"`이며 기본 API만으로 Primary가 적용된다.
|
||||
|
||||
```tsx
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui"
|
||||
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList aria-label="업무 보기">
|
||||
<TabsTrigger value="overview">개요</TabsTrigger>
|
||||
<TabsTrigger value="details">상세</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview" className="border bg-background rounded-b p-4">개요 콘텐츠</TabsContent>
|
||||
<TabsContent value="details" className="border bg-background rounded-b p-4">상세 콘텐츠</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
- **Primary**: `<TabsList>`, `<TabsList fill>`, `<TabsList fill panelSurface="content">`는 같은 디자인이다. 선택 면은 White → `--hm-sem-bg-content` Gradient다.
|
||||
- **Secondary 명시적 사용**: 위 예제의 List를 `<TabsList fill panelSurface="surface" aria-label="업무 보기">`로 바꾸고 **각** `TabsContent`에 `border bg-card rounded-b p-4`를 사용한다. 선택 면은 기존 White surface다.
|
||||
- **Legacy non-fill 명시적 사용**: `<TabsList fill={false} aria-label="업무 보기">`와 **각** `TabsContent`의 `border bg-card rounded-b p-4` 조합으로 이전 스타일을 유지한다. `<TabsList fill={false} panelSurface="surface">`도 같은 시각이다. OLD_TEST_ONLY / REMOVE_CANDIDATE 이력은 유지하며 별도 승인 없이 삭제하지 않는다.
|
||||
|
||||
`TabsContent`는 패널 배경·테두리를 자동으로 지정하지 않는다. 소비자가 Primary에는 `bg-background`(content), Secondary / Legacy에는 `bg-card`(surface)를 연결한다. `panelSurface`는 List의 선택 면·연결 곡선에만 적용되며 콘텐츠 색을 바꾸지 않는다. Bar와 패널 사이에 gap이나 별도 구분선을 넣지 않는다. 상태·Variant는 §4.1, 외부 소비자 변경 안내는 §15를 따른다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 기본 Primary Fill + 명시적 Secondary Fill + Legacy non-fill + 넘침 이동 버튼(D-090) 구현 (`hm-tab` class 없음) |
|
||||
| 기준일 | 2026-10-08 (Primary 기본값 승인 반영) |
|
||||
| 기반 | shadcn/ui Tabs (Radix `Tabs`) |
|
||||
| 대표 Story | `Components/Navigation/Tabs` → `All` (이름 "Tabs"): Primary(Fill + content) / Secondary(Fill + surface) |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격(`docs/components/tabs.md`, `brand/hanmac/components/tab.md`)의 기본 profile, `fill` 콘텐츠 전환 profile, 넘침 이동 버튼의 디자인 의도와 수치.
|
||||
- **현재 HM 규격**: Primary / Secondary는 §4.1, Legacy non-fill은 §3~§4. 수치는 ui-viz에서 가져왔지만 HM 구현에서 다시 검토해 유지한 것이다(아래 검토 결과).
|
||||
- **구현 방식**: shadcn / Radix Tabs 구조(roving focus, 방향키·Home/End, 자동 활성화, ARIA)를 유지하고 theme CSS로 HM 스타일을 연결. `TabsList`가 frame과 이동 버튼을 렌더.
|
||||
- **보류**: §11.
|
||||
|
||||
**ui-viz 비확정 값 검토 (2026-10-02)**: Default 면(`green-tint` 45%)·Default 테두리(`green-light-2` 35%)는 ui-viz에서 비확정 표기였으나 HM Primitive 팔레트의 값을 투명도로 합성한 것이고 Selected(White)와의 대비가 분명해 HM 방향에 맞는다고 판단, 현재 HM 규격으로 유지한다. 곡선 반경 8px도 같은 이유로 유지한다. 단 값은 디자인 확정 시 조정될 수 있다. ui-viz의 볼록 gradient(미구현), 13px 라벨(미채택)은 가져오지 않았다.
|
||||
|
||||
## 3. Legacy non-fill 규격 (`fill={false}`)
|
||||
|
||||
이 표의 배치·인접 경계는 이전 non-fill profile이다. 높이·padding·radius·Typography·Bar 규격은 Fill에서도 공유하지만, 균등 폭·인접 경계·선택 면·연결 곡선은 §4.1을 따른다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Tab 높이 | 34px (`height` + `min-height`), Button·radius token과 alias 없음 |
|
||||
| Padding | `0 16px` |
|
||||
| Radius | `4px 4px 0 0` |
|
||||
| Border | 1px. 인접 탭은 `border-left: 0`으로 단일 경계 |
|
||||
| Typography | 16 / 24 / 700 (Selected 동일, 구분은 색·면·테두리로) |
|
||||
| 배치 | `flex-end` 정렬, gap 0, nowrap, `flex: none`, 각 탭 `position: relative` |
|
||||
| Bar | 자체 표면·padding·border·radius 없음, `margin-bottom: -1px`, `overflow-x: auto`, scrollbar 숨김, `max-width: 100%` |
|
||||
|
||||
크기·variant 축 없음 (ui_viz Out of Scope). 방향은 horizontal 하나.
|
||||
|
||||
## 4. Legacy non-fill 상태 규격 (`fill={false}`)
|
||||
|
||||
`panelSurface="surface"` / `"content"`는 이 상태표에 시각적 영향을 주지 않는다. 아래 Default는 비선택 상태를 뜻하며 기본 Variant를 뜻하지 않는다.
|
||||
|
||||
| 상태 | 면 | 테두리 | 전경 |
|
||||
|---|---|---|---|
|
||||
| Default (`data-state="inactive"`) | `rgba(189,223,212,.45)` | top/right `rgba(38,102,81,.35)`, bottom `#DEDBD2` | `#266651` |
|
||||
| Selected (`data-state="active"`) | `#FFFFFF` (`--hm-sem-bg-surface`) | top/right/left `#DEDBD2`, bottom 1px 표면색(기준선을 덮어 콘텐츠와 연결), `z-index: 2` | `#123328` |
|
||||
| Hover (비선택만, `:hover`) | 기본 면 + Black 20% overlay (`linear-gradient`) | 변화 없음 | `#123328` |
|
||||
| Hover (선택) | 적용 안 함 | — | — |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 | — | — |
|
||||
| Disabled | shadcn 기본 `opacity-50`, 클릭 불가 | — | — |
|
||||
|
||||
전환: 면·전경 0.15s ease (테두리·geometry는 전환하지 않음). Selected 하단 stroke는 indicator가 아니라 연결 수단이다.
|
||||
|
||||
### 4.1 현재 Fill 디자인: Primary / Secondary State · Variant
|
||||
|
||||
- **Primary (기본값)**: `fill=true`, `panelSurface="content"`. Selected는 White → bg-content Gradient, top / left / right는 Green Light 3(`--hm-tab-fill-line-color`), bottom은 content 패널색이다.
|
||||
- **Secondary**: `fill=true`, `panelSurface="surface"`. Selected는 기존 White surface 면, 같은 Green Light 3 테두리, bottom은 surface 패널색이다.
|
||||
- **공통 Geometry**: 기존 높이 34px, Typography 16/24/700, 좌우 padding 16px, 상단 radius 4px, 선택 탭의 8px 곡선 연결을 유지한다. Overflow가 없으면 균등 폭, 모든 Fill 이웃은 1px left border와 `margin-left: -1px`로 경계를 공유한다. Overflow이면 라벨 필요 폭을 유지한다.
|
||||
- **Inactive**: 면·top / vertical border·전경은 §4의 기존 비선택 값, bottom line만 Green Light 3다.
|
||||
- **Hover**: 비선택·활성화 가능한 탭에만 기존 Black 20% overlay와 전경 전환을 적용한다. Selected Gradient / White 면은 유지한다.
|
||||
- **Focus / Disabled**: 기존 shadcn focus-visible ring / Disabled opacity 0.5·클릭 불가·Radix 키보드 건너뛰기를 유지한다. 선택·ARIA·키보드 동작은 §7, Overflow 이동은 §5·§8을 따른다.
|
||||
|
||||
## 5. API
|
||||
|
||||
shadcn/Radix 그대로. `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (Radix `Tabs.Root/List/Trigger/Content` props). 추가(기존 API 무변경, 모두 선택):
|
||||
|
||||
```
|
||||
TabsList:
|
||||
fill?: boolean 기본 true. Primary / Secondary Fill; false는 Legacy non-fill
|
||||
panelSurface?: "surface" | "content" 기본 "content". Fill 선택 면이 이어지는 panel 바탕 (임의 색 미지원 = TABS_PANEL_SURFACE_ENUM)
|
||||
```
|
||||
|
||||
- `TabsList`는 Radix list를 감싸는 frame(`data-slot="tabs-frame"`)을 렌더한다(ui_viz `.hm-tabs-frame`과 같은 구조). `role`·`aria-*`·`className`·ref·나머지 props는 tablist로 간다. frame에 `data-overflow`, `data-fill`, `data-panel-surface`.
|
||||
- `panelSurface`는 `fill=false`이면 시각적으로 무시한다. Props 이름·타입·선택 여부와 공개 export는 유지하며 기본 렌더링만 변경한다(§15).
|
||||
- 넘칠 때만 frame 안에 이동 버튼 2개(`data-slot="tabs-arrow"`, `aria-label` 「이전 탭 보기」「다음 탭 보기」, `aria-controls`=tablist id)를 tablist 밖에 렌더한다. Tab 순서: 이전 버튼 → 선택 tab → 다음 버튼, 끝 방향 버튼은 `disabled`.
|
||||
|
||||
- `value`/`defaultValue`/`onValueChange`, `disabled`, `activationMode`, `orientation` 등은 Radix 동작 그대로다. ui_viz의 controlled-only `<Tabs value onValueChange><Tab/></Tabs>` API는 복제하지 않는다.
|
||||
- 출력 속성: `data-slot="tabs|tabs-list|tabs-trigger|tabs-content"`, Radix `data-state`·`data-orientation`.
|
||||
- `TabsList`에는 `aria-label` 또는 `aria-labelledby`를 넘겨야 한다(ui_viz §6).
|
||||
- 이동 버튼은 기존 `Button`을 쓰지 않는 Tabs 전용 control이다.
|
||||
- Selected가 이어지는 콘텐츠 표면은 소비자의 panel이다. Tabs가 자체 표면·구분선을 만들지 않는다. Tab Bar와 콘텐츠 사이에 구분선을 넣으면 연결이 깨진다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/tabs.css` (`--hm-tab-*`).
|
||||
|
||||
- Tabs 전용: `--hm-tab-{height,padding-x,radius-top,border-width}`, `--hm-tab-font-{size,weight}`, `--hm-tab-line-height`, `--hm-tab-{background,border-color,text}`, `--hm-tab-selected-{background,border-color,text}`, `--hm-tab-hover-{overlay,text}`, `--hm-tab-transition-duration`.
|
||||
- 재사용한 공통 값: `--hm-sem-bg-surface`(Selected 면), 역할 Semantic `--hm-sem-action-primary`, `--hm-sem-stroke-input-default`, Theme palette `--hm-sem-palette-green-tint`, `--hm-sem-palette-green-light-2`, `--hm-sem-palette-black`. Component Token은 독립적으로 유지한다. Primitive 직접 참조 설명은 초기 구현 이력이다.
|
||||
- 이동 버튼: `--hm-tab-arrow-{size,icon-size,background,border-color,icon-color,disabled-opacity}`. fill: `--hm-tab-fill-line-color`(→ `--hm-sem-palette-green-light-3`), `--hm-tab-fill-curve-radius`(8px, REVIEW VALUE), 내부 `--hm-tab-fill-{panel,face}`.
|
||||
- **초기 Primitive 추가 이력**: `--hm-green-light-3: #249473` (ui-viz 팔레트의 Green Light 3). 현재 Tabs fill은 공개 Theme palette `--hm-sem-palette-green-light-3`를 통해 참조한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving tabindex·방향키 순환·Home/End·자동 활성화, `data-slot`, `cn(..., className)`, trigger의 focus ring, Disabled 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 list `bg-muted`·rounded·padding, trigger의 흰색 pill/dark 스타일, root `gap-2`, list variant(`default`/`line`) 제거 → theme CSS가 담당.
|
||||
- Bar는 콘텐츠와 gap 없이 붙는다.
|
||||
- `TabsList`가 frame wrapper와 이동 버튼을 렌더(ui_viz 구조 반영, 프로젝트 지침상 필요한 wrapper).
|
||||
- 선택 tab을 보이게 유지: click/keyboard 선택, resize·글꼴 로딩·항목 변경 시 list `scrollLeft` 보정(ResizeObserver/MutationObserver).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary, **Default API**는 `fill`만 명시한 Primary와 `fill` + `panelSurface="content"`를 명시한 Primary를 비교한다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다.
|
||||
- verification(`tags: ["verification"]`): `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다.
|
||||
- `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다.
|
||||
- non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10).
|
||||
- 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다.
|
||||
- Story의 콘텐츠 panel은 Primary `TabsContent` + `border bg-background`, Secondary / Legacy `TabsContent` + `border bg-card` 소비자 조합 예시다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-06: Tabs의 최종 디자인 언어는 **Primary = Fill + `panelSurface="content"`**(기본형), **Secondary = Fill + `panelSurface="surface"`**(서브형) 두 가지다. 이 결정이 ui_viz보다 우선한다. non-fill 기본 profile은 OLD_TEST_ONLY로 분류해 대표 Story에서 내렸다.
|
||||
- **2026-10-06 당시 범위**는 Storybook 대표형에 한정했다. 당시 Public API 기본값(`fill=false`, `panelSurface="surface"`)과 non-fill CSS branch는 유지하고 기본값 변경·branch 삭제를 별도 승인 대상으로 남겼다. 당시 저장소 내 소비처가 없다는 관찰은 외부 소비자의 무영향을 보장하지 않는다.
|
||||
- **2026-10-08 추가 개선 E 승인**: `TabsList` 기본값을 `fill=true`, `panelSurface="content"`로 변경한다. Primary / Secondary / Legacy의 기존 CSS 디자인 값·선택자·동작·Theme / Token을 유지한다. non-fill의 OLD_TEST_ONLY / REMOVE_CANDIDATE 이력과 명시적 사용 API는 유지하며 삭제 승인은 포함하지 않는다. `panelSurface`는 Fill일 때만 효과가 있는 List prop이다(`Tabs` Root prop이 아님).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 이동 후 가장자리 정렬 세부 (다음 버튼은 오른쪽에 걸친 첫 항목의 끝을 보이는 영역 끝에 맞춤), 이동 버튼 포커스 수치(global focus TBD, box-shadow 3px 링 사용), fill에서도 Bar −1px 겹침 유지, Brown 콘텐츠 탭·dark mode TBD.
|
||||
- `panelSurface`는 `surface`/`content`만 지원(현재 제한).
|
||||
- Default 면·테두리 합성색과 곡선 반경 8px는 디자인 확정 시 재검토.
|
||||
- Focus 수치: global focus `TBD`. shadcn ring 유지.
|
||||
- Disabled: ui_viz P0 제외. shadcn 기본.
|
||||
- 볼록 Gradient: `TABS_CONVEX_GRADIENT_KNOWN_DEVIATION`, 평면으로 출하(구현도 평면).
|
||||
- Typography: expanded Source의 13px는 `TABS_SOURCE_LABEL_SIZE_13_UNADOPTED`로 16px 유지.
|
||||
- 앞 탭 right border가 2번째 이후 Selected의 좌측을 대신 그리는 관계는 ui_viz `BASELINE_BEHAVIOR_ACCEPTABLE`이며 그대로 구현.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/tabs.tsx
|
||||
packages/ui/src/theme/tabs.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Tabs, TabsContent, TabsList, TabsTrigger)
|
||||
src/components/tabs.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 추가 개선 E: 기존 6024 Storybook 재사용. 대표 `All`에서 기본 Props / `fill`만 명시 / 두 Props 명시가 모두 Primary(`data-fill="true"`, `data-panel-surface="content"`)이며 같은 Gradient·Green Light 3 경계·34px·16/24/700·8px 곡선을 확인했다. Secondary의 White 면·패널과 Bar / panel 1px 겹침도 유지했다.
|
||||
- 숨김 `Compatibility`: `fill={false}`의 content / surface 조합은 같은 White 선택 면·Legacy 경계·라벨 필요 폭이고 곡선이 없다. `panelSurface="surface"`만 명시하면 Secondary다. CSS 주석을 제외한 모든 규칙·값과 Production의 기본값 두 개를 제외한 코드가 작업 시작 상태와 동일함을 비교했다.
|
||||
- 실제 포인터 / 키보드: 클릭·ArrowLeft / Right·Home / End 선택과 콘텐츠 전환, 비선택 Hover Black 20% / 선택 Gradient 유지, focus-visible 3px 링, Disabled opacity 0.5·pointer-events none·tabIndex -1·방향키 건너뛰기를 확인했다. 독립 Overflow Story는 260px frame / 192px list에서 다음 이동 `scrollLeft 0→33.33`, 이전 이동 `→0`, 끝 버튼 Disabled 전환과 선택 유지가 정상이다.
|
||||
- 1280px / 390px: 대표 Story의 문서 가로 넘침 없음. 390px의 기본·명시적 Fill frame은 약 342.67px, Overflow specimen은 260px. 390px Compatibility도 문서 가로 넘침 없이 Legacy list 약 226px를 유지했다. HM Theme의 content / surface / Green Light 3 연결은 유지한다.
|
||||
- `npm run build:ui`(UI 패키지·선언 빌드), `node node_modules/typescript/bin/tsc -p tsconfig.app.json`(Story 포함) 통과. Tabs 변경 파일의 diff whitespace 검사 통과. 전체 회귀·전체 Storybook 정적 빌드는 이번 범위에서 실행하지 않았다.
|
||||
- Console: 패키지 dist 재생성 중 기존 브라우저에 Vite `src/index.css` reload 오류가 한 번 기록됐으나 복구됐다. 빌드 후 새 브라우저에서 대표 / Compatibility를 확인한 Console error / warning은 0건이다. 외부 소비자 전체 호환성은 §15의 한계를 따른다.
|
||||
- 2026-10-04: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. build의 기존 Tailwind 문서 예시 경고(`bg-[var(...)]`)와 Storybook 500kB chunk 경고 외 신규 경고 없음.
|
||||
- Storybook computed style(`components-navigation-tabs--all`): 높이 34px, padding `0 16px`, radius `4px 4px 0 0`, 16/24/700, Default/Selected 색과 연결선 일치. fill 선택 경계 `rgb(36,148,115)`, 선택 면과 8×8 곡선 확인.
|
||||
- 실제 pointer + keyboard: 비선택 hover Black 20% overlay·전경 전환, 선택 hover 비적용, 클릭·ArrowRight/Left·Home/End 자동 선택, 3px focus-visible ring, `tabpanel` 내용 전환과 54px panel 높이 유지.
|
||||
- Disabled: `disabled=true`, `tabIndex=-1`, opacity 0.5, 활성화 불가.
|
||||
- Overflow: 260px frame에서 192px list / 605px scroll width, 이동 버튼 1회 `scrollLeft 0→36`, 선택값 유지, 이전 버튼 활성화.
|
||||
- Long Label: `nowrap`, `text-overflow: clip`, 단일/복수 긴 라벨에서 실제 필요 폭 기준 overflow. 가시 영역보다 넓은 선택 탭은 시작점을 목록 왼쪽에 맞춘다.
|
||||
- Responsive: 1280 / 768 / 390px 모두 문서 가로 overflow 없음(`scrollWidth == clientWidth`). 390px에서 기본·fill은 frame 343px, 명시적 narrow specimen은 260px로 유지.
|
||||
- 2026-10-06 smoke check(ui_viz Storybook 나란히): fill 인접 경계(`margin-left:-1px` + 1px 경계)·선택 탭 8×8 곡선 ±8px 접점·34px·16/24/700·색 유지. Overflow/Fill Overflow/Long Label은 3개 viewport에서 이동 버튼 전환, 선택 탭 시작점 왼쪽 정렬, 페이지 넘침 없음. Click·ArrowRight·Home 동작 확인. 새 회귀 없음.
|
||||
- 2026-10-08 저장소 소스 검색: Production / Dashboard에는 TabsList 소비처가 없으며 `src/components/tabs.stories.tsx`만 소비한다. 외부 프로젝트는 이 확인 범위에 포함하지 않는다.
|
||||
|
||||
## 14. ui-viz 직접 비교 (2026-10-04)
|
||||
|
||||
2026-10-06 비교는 외부 ui_viz의 보존된 두 원본 사본의 Tabs JSX/CSS SHA-256 일치를 확인한 뒤 동일 소스와 당시 캡처를 참고했다. 외부 자료는 ZIP에 포함되지 않으며 현재 Source of Truth는 HM 구현·대표 Story와 이 문서다. 당시 백업 Storybook은 dependency가 제거되어 실행하지 못했으므로, 비교 범위는 보존된 소스·캡처와 HM computed value였다.
|
||||
|
||||
| 항목 | ui-viz | 수정 전 HM | 판정 / 최종 처리 |
|
||||
|---|---|---|---|
|
||||
| 구조·상태 | 자체 controlled Tabs/Tab + roving focus | Radix Root/List/Trigger/Content | `IMPLEMENTATION_CHANGE`; Radix 유지 |
|
||||
| 기본 geometry·색·type | 34px, 0 16px, 4px, 16/24/700, 연결형 border | computed value 일치 | `MATCHED` |
|
||||
| hover / selected | inactive만 Black 20%, selected surface 연결 | 일치 | `MATCHED` |
|
||||
| focus / disabled | 전역 수치 미정 / P0 제외 | 명확한 Radix focus ring / opacity 0.5 | `INTENTIONAL_CHANGE` |
|
||||
| fill 인접 경계 | 모든 이웃이 `margin-left:-1px` + left border 복원 | active 이웃만 부분 복원 | `REGRESSION`; 원본 겹침 관계 복구 |
|
||||
| fill 곡선 접점 | active edge에서 정확히 ±8px | edge 밖으로 1px 이격 | `REGRESSION`; ±8px 접점 복구 |
|
||||
| overflow | 실제 필요 폭, 34px 양끝 control, 한 항목 이동 | 수치·동작 일치 | `MATCHED` |
|
||||
| 매우 긴 선택 탭 | item이 viewport보다 넓으면 시작점 left align | right align으로 라벨 앞부분이 가려짐 | `REGRESSION`; left align 복구 |
|
||||
| Long Label 검증 | 실제 사용 specimen 존재 | 전용 Story 없음 | `MISSING`; 숨김 verification Story와 대표 구역 추가 |
|
||||
| controlled-only API·13px 실험 라벨 | ui-viz Web mapping/미채택 값 | Radix 양방향 API·16px | `OLD_TEST_ONLY`; 미도입 |
|
||||
|
||||
### Before / After
|
||||
|
||||
| 항목 | Before | After | ui-viz |
|
||||
|---|---|---|---|
|
||||
| fill 이웃 경계 | 후속 inactive left border 0 | left border 1px + margin-left -1px | 동일 |
|
||||
| fill 곡선 | active edge와 1px gap | edge 기준 `-8px` | 동일 |
|
||||
| viewport보다 넓은 선택 tab | 오른쪽 끝 정렬 | 왼쪽 시작점 정렬 | 왼쪽 시작점 정렬 |
|
||||
| Long Label Story | 없음 | 단일/복수/260px 검증 | 사용 specimen 존재 |
|
||||
|
||||
## 15. 외부 소비자 영향 / 호환성 (2026-10-08)
|
||||
|
||||
- **변경 전**: `<TabsList>` → Legacy non-fill (`fill=false`, `panelSurface="surface"`).
|
||||
- **변경 후**: `<TabsList>` → Primary Fill (`fill=true`, `panelSurface="content"`).
|
||||
- **이전 스타일 유지**: `<TabsList fill={false}>`. 이전 White 패널 조합(`bg-card`)도 유지한다. `panelSurface="surface"`를 함께 명시해도 non-fill 시각은 같다.
|
||||
- **`fill`만 명시한 기존 소비자**: 이전에는 Secondary(surface), 이제는 Primary(content)가 된다. 이전 Secondary를 유지하려면 `<TabsList fill panelSurface="surface">`와 surface 패널을 사용한다.
|
||||
- **`panelSurface`만 명시한 기존 소비자**: 이전에는 non-fill이라 시각적으로 무시했지만 이제 Fill이 적용된다. `"surface"`는 Secondary, `"content"`는 Primary다. non-fill을 유지하려면 `fill={false}`를 추가한다.
|
||||
- **두 Props 모두 명시**: 기존 조합의 시각은 유지한다. Props 이름·타입·Radix props·export·DOM 구조는 변경하지 않는다. 타입 호환성과 기본 디자인 호환성은 구분한다.
|
||||
- **레이아웃 차이**: non-fill은 라벨 필요 폭, Fill은 Overflow가 없을 때 컨테이너 전체를 균등 분할한다. 높이 34px은 같으나 탭 폭·공유 경계·선택 연결 곡선·패널색이 달라질 수 있다. 좁은 폭에서는 기존 Overflow 이동 버튼이 나타날 수 있다.
|
||||
- **CSS Override 영향**: frame의 기본 attribute가 `data-fill="false"` / `data-panel-surface="surface"`에서 `"true"` / `"content"`로 바뀐다. non-fill 조건 selector, list width / flex, active background / border, 곡선 pseudo-element override를 사용하는 소비자는 검토가 필요하다. `className`과 ref는 계속 tablist에 전달된다.
|
||||
- **확인 범위**: 현재 저장소의 TabsList 사용은 Story만 확인했다. 외부 소비자 전체의 명시적 Props·패널색·레이아웃·CSS Override는 실제 확인하지 못했으므로 위 내용은 영향 가능성과 이행 안내이며 외부 호환성 전체 검증을 뜻하지 않는다.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Textarea
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Textarea 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-textarea` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Textarea (native `<textarea>`) |
|
||||
| 대표 Story | `Components/Form/Textarea` → `All` (이름 "Textarea") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
원본: 외부 ui_viz Reference (ZIP 미포함)의 `docs/components/textarea.md` (+ `brand/hanmac/foundation/{color,border}.md`, `foundation/token-inventory.md`의 입력 계열 Semantic).
|
||||
|
||||
- **ui_viz Confirmed**: border 1px / radius 4px(§2, §2.2), 면·값·Placeholder 색(§2.2), 기본 테두리 `color.stroke.input.default`, Focus `color.stroke.input.focus`, Error `color.stroke.input.error`, Focus/Error 두께 1.5px(D-005 상속, §2.2), Hover = Default(§3.3 사용자 결정 TEXTAREA-05, Confirmed), Readonly 표현(§3.1), 값 typography 16/24/400(§2).
|
||||
- **SCOPED_DESIGN_APPROVAL** (2026-09-17 owner, Textarea 한정): min-height `112px`, padding `8px 12px`(§8.1, TEXTAREA-01/02). D-093 디자인 확정값이 아니며 ui_viz Canonical은 `TBD`다. Textarea 범위에서만 사용하고 `size.*`/`space.*`로 승격하지 않는다.
|
||||
- **Component-local**: `resize` 정책(vertical 기본, none/both 선택, Readonly는 none 고정).
|
||||
- **shadcn 유지**: native textarea 동작, `transition-colors`, `outline-none`, Disabled opacity와 커서.
|
||||
- **HM 사용자 결정(2026-10-06)**: Error border와 Keyboard Focus ring을 독립 Semantic으로 표시한다.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 상태 |
|
||||
|---|---|---|
|
||||
| Width | 부모 폭 100% (`w-full`) | Canonical |
|
||||
| Min-height | 112px | SCOPED_DESIGN_APPROVAL |
|
||||
| Padding | 8px (block) / 12px (inline) | SCOPED_DESIGN_APPROVAL |
|
||||
| Border / Radius | 1px `--hm-sem-stroke-input-default` / `--hm-radius-input`(4px) | Confirmed |
|
||||
| Face / 값 색 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` | Confirmed |
|
||||
| Typography | 16 / 24 / 400 | Existing role |
|
||||
| Placeholder | `--hm-sem-fg-placeholder`, `opacity: 1` | Confirmed |
|
||||
| Resize | `vertical` 기본 | Component-local |
|
||||
|
||||
Size 체계 없음. Input의 Small/Medium/Large와 공유하지 않는다(ui_viz Textarea에 size 정의 없음). `field.gap` 8px 등 Field 관계는 Textarea 밖(소비 조합)이다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | Confirmed |
|
||||
| Hover | Default와 동일 (hover rule 없음) | Confirmed (사용자 결정 TEXTAREA-05) |
|
||||
| Focus (`:focus-visible`) | border 1.5px `--hm-sem-stroke-input-focus`, shadow/glow 없음, padding으로 0.5px 보정해 글자 위치 유지 | Confirmed (D-005 상속) |
|
||||
| Error (`[aria-invalid="true"]`) | border 1.5px `--hm-sem-action-destructive`, ring 없음, 동일 padding 보정 | Confirmed (D-005 상속) |
|
||||
| Error + Keyboard Focus | Destructive border 유지 + HM Focus Semantic 3px / 50% outer ring, `outline: none` | Input과 동일, 2026-10-06 사용자 결정 |
|
||||
| Disabled | surface face 유지, `cursor: not-allowed`, `opacity: 0.6` | opacity: 사용자 결정(§10) |
|
||||
| Readonly (`[readonly]`) | Default와 같은 면·테두리·typography, `opacity: 1`, `resize: none` 고정 | Component-local (§3.1) |
|
||||
|
||||
Chromium은 1.5px 테두리를 `getComputedStyle` 기준 1px로 계산한다(ui_viz도 같은 관측을 기록). padding 보정(7.5px/11.5px)은 선언 1.5px 기준으로 적용된다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Textarea:
|
||||
resize?: "vertical" | "none" | "both" 기본 "vertical"
|
||||
```
|
||||
|
||||
- 나머지는 native `<textarea>` props 그대로(`rows`, `placeholder`, `disabled`, `readOnly`, `aria-invalid` 등).
|
||||
- 출력 속성: `data-slot="textarea"`, `data-resize`.
|
||||
- 상태는 새 prop이 아니라 native 속성으로 준다.
|
||||
- Field(Label, Required 별표, Helper/Error 문구)는 Textarea에 포함하지 않는다. 소비 조합이다.
|
||||
- auto-grow, 글자 수 카운터는 범위 밖이다. shadcn의 `field-sizing-content`(auto-grow)는 제거했다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/textarea.css` (`--hm-textarea-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`, `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`, `--hm-sem-action-destructive`(Semantic 참조).
|
||||
- Textarea 전용 값: `--hm-textarea-min-height`, `--hm-textarea-padding-{block,inline}`, `--hm-textarea-font-{size,weight}`, `--hm-textarea-line-height`, `--hm-textarea-border-width`, `--hm-textarea-state-border-width`(1.5px).
|
||||
- Primitive 신규 추가 없음.
|
||||
- Input의 `--hm-input-*`와 값이 겹쳐도 alias하지 않는다(ui_viz도 Input·Textarea를 별도 owner로 두고 값을 각각 소비).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
native `<textarea>` 동작 전체, `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none`, `disabled:cursor-not-allowed`. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `resize` prop, `data-resize` 추가.
|
||||
- shadcn의 기본 min-height/padding/border/ring/shadow/`dark:`/`field-sizing-content` class 제거 → theme CSS가 담당.
|
||||
- 일반 Focus는 border 1.5px로 표시, Error 단독은 ring 없음. Error + `:focus-visible`만 독립 Focus ring을 표시한다.
|
||||
- Readonly 전용 표현(resize 고정) 추가.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Textarea"): 구역 `default`, `hover`, `focus`, `error`, `error-message`, `readonly`, `disabled`, `multiline`, `resize`. Focus는 `play` 함수로 실제 focus를 준다. Hover는 별도 pseudo 복제 없이 Default와 같다는 설명만 둔다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Readonly`, `Disabled`, `LongContent`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Textarea 컴포넌트의 일부가 아니다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**: Textarea Disabled opacity `0.6` 확정. ui_viz에서 `Temporary`지만 현재 HM 구현에서는 이 결정이 우선한다. Textarea 한정이며 전역 Disabled opacity로 확대하지 않는다 (Input·Select와 같은 값). Token이 아니라 `ui/textarea.tsx`의 `disabled:opacity-60`으로 적용.
|
||||
- ui_viz에 기록된 결정을 따른다: 2026-09-17 Hover = Default(TEXTAREA-05), min-height 112 / padding 8·12 Textarea 한정 승인(TEXTAREA-01/02). 2026-10-01 필수 별표 `#D32F2F`(TEXTAREA-07, Textarea에 별표 없음 — Story 조합 예시는 shadcn `text-destructive`).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **min-height 112 / padding 8·12**: Textarea 한정 owner 승인이지만 ui_viz Canonical은 `TBD`, D-093 미확정(TEXTAREA-01/02/06, `NO_SOURCE_EVIDENCE`). 변경 가능성이 있다.
|
||||
- **Label 14/20/500 · Value 16/24 · Error 11/18 vs D-093 원본 실행본 13/700 등**: `KNOWN_DEVIATION / FOLLOW_UP`(TEXTAREA-08). 확정 근거 없어 ui_viz 문서값을 사용.
|
||||
- **Error + Focus**: HM 사용자 결정(2026-10-06)으로 Input과 같은 3px / 50% Focus Semantic ring을 적용한다.
|
||||
- auto-grow, 글자 수 카운터(TEXTAREA-03): 범위 밖. max length 관련 표현 없음.
|
||||
- 전역 focus ring 수치: TBD.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/textarea.tsx
|
||||
packages/ui/src/theme/textarea.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Textarea, TextareaResize)
|
||||
src/components/textarea.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`(root + ui + center-graphic), `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(`components-form-textarea--all`): Default min-height 112px / padding 8px 12px / border 1px rgb(222,219,210) / radius 4px / bg white / 16·24·400 / resize vertical / Placeholder rgb(153,153,153), Error border rgb(211,47,47) + padding 7.5px 11.5px, Readonly resize none · opacity 1, `resize="none"` 적용, Disabled opacity 0.6.
|
||||
- 실제 클릭 focus: `:focus-visible` 매칭, border rgb(18,51,40), padding 7.5px 11.5px 확인.
|
||||
- Story ID: `All` + verification 8개 존재 확인(index.json). `@hm-design/ui`에서 `import { Textarea }`로 Story가 빌드됨.
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): 컨트롤 computed style 일치(ui_viz Story의 114px은 `rows=4` 때문), Error+Focus outline 유지, 긴 내용은 내부 세로 scroll·가로 넘침 없음, Readonly 편집 불가·선택 가능, Disabled focus 불가. ui_viz는 Disabled에서 Field 전체(라벨 포함)를 흐리지만 HM은 컨트롤만 흐리고 라벨은 `Label`의 disabled 연동(소비 조합)이 맡는다 — 의도적 차이. 대표 Story는 ui_viz 5구역을 모두 포함한 상위 집합(Hover = Default, Error Message, Resize none 추가)이라 변경하지 않음.
|
||||
- 미확인: 브라우저별 렌더(Chromium 외).
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 대표 `All`과 숨김 `InvalidFocus` 확인. 일반 Focus / Invalid의 padding 7.5px 11.5px, 기본 min-height 112px / 16px 글자 / Readonly resize none / Disabled opacity 0.6 유지. Border CSS 계약 1.5px, 이 브라우저 환경의 computed border-width는 1px으로 정규화. Input과 같은 Invalid + Focus ring.
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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`·`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.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Toggle
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Toggle 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Toggle (Radix `Toggle`) |
|
||||
| 대표 Story | `Components/Form/Toggle` → `All` (이름 "Toggle") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Toggle이 없다(인벤토리에 Switch 계열 기능 이름으로만 언급). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Toggle 구조와 `variant`(default | outline), `size`(default | sm | lg) API를 유지하고, 모양은 HM 독립 Token(`--hm-toggle-*`)으로 정한다.
|
||||
- **Button과의 관계**: 이 문서 §14.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `sm` 28px, `default` 34px, `lg` 42px (높이). padding-x 8 / 12 / 16px |
|
||||
| Icon-only | 단일 React element + 직계 `svg` 하나인 경우 `data-icon-only`와 CSS로 padding 0. SVG + text node는 size별 padding 8/12/16px 유지 |
|
||||
| Radius | `--hm-radius-control-md` (4px) |
|
||||
| Typography | 14 / 20 / 500, icon + text는 gap 8px |
|
||||
| Off | 배경 투명, 글자 `--hm-sem-fg-primary` |
|
||||
| On | 배경 `--accent`, 글자 `--accent-foreground` (full Accent; Select 선택 항목의 Accent 45% 면과는 별도 정책) |
|
||||
| `outline` variant | 1px `--hm-sem-stroke-input-default` border(`default`는 border 없음) |
|
||||
| 커서 | pointer |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Off / On | Radix `data-state="off" | "on"`, `aria-pressed` |
|
||||
| Hover (off) | 배경 `--accent` 45% (on 상태는 그대로 On 면) |
|
||||
| Selected + Hover (on) | 배경 `--accent` 100%, 글자·아이콘 `--accent-foreground` 유지. Hover 45% 면으로 내려가지 않음 |
|
||||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, 클릭·hover 무반응 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Toggle props 그대로. export: `Toggle`, `toggleVariants`.
|
||||
|
||||
- `pressed`/`defaultPressed`/`onPressedChange`, `disabled`, `asChild`.
|
||||
- `variant` `"default" | "outline"`, `size` `"default" | "sm" | "lg"`(shadcn 호환). 출력 `data-variant`, `data-size`, `data-slot="toggle"`.
|
||||
- icon-only Toggle은 소비자가 `aria-label`을 준다(Story 참고).
|
||||
- 새 variant는 만들지 않았다(`default`, `outline` 두 개만 유지).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/toggle.css` (`--hm-toggle-*`).
|
||||
|
||||
`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`, `outline-border-width|color`, `disabled-opacity`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. `--hm-button-*`와 Button의 role 체계는 참조하지 않는다. 신규 Primitive와 Semantic 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·`aria-pressed`·`data-state`·Enter/Space·controlled/uncontrolled·disabled·`asChild`, shadcn `variant`/`size` API와 `toggleVariants`(cva), focus ring class, svg 정렬 class, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `toggleVariants`의 색·크기 class(`h-8 min-w-8 px-2.5`, `hover:bg-muted`, `data-[state=on]:bg-muted`, outline `border-input shadow-xs`)를 제거하고 theme CSS로 이전. On 면은 `muted`가 아닌 accent.
|
||||
- 기본 모양을 `@layer components`에 두어 소비자의 `className`(폭, radius, 레이아웃 utility)이 이긴다. 다른 theme CSS(레이어 밖)와 달리 Toggle은 Story에서 `className="w-40 rounded-full"`이 적용되는 것을 확인했다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Toggle"): Off/On(default·outline), Icon-only와 icon + text(`BoldIcon`, `ItalicIcon`, `StarIcon`), Sizes, Disabled(off/on), Controlled(`pressed` 상태 표시), className 확장을 한 페이지에 둔다.
|
||||
- verification: `States`, `Icon Only`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Toggle은 Button과 독립 Component Token을 쓴다(작업 지시).
|
||||
- 2026-10-06 Selected 원복: Git baseline `d101530`의 `--accent` / `--accent-foreground` 매핑을 복원했다. 아이콘은 `currentColor`로 전경을 따른다. Border는 baseline대로 default 투명 / outline `--hm-sem-stroke-input-default`이며 Selected 전용 border는 없다. 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, padding, On 면 accent, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- On 상태 hover는 On 면 그대로이다(별도 hover 면을 정하지 않음).
|
||||
- On 상태를 Primary(진한 녹색 면)로 강하게 표시하는 variant는 만들지 않았다.
|
||||
- 아이콘 크기는 shadcn 기본(16px) 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/toggle.tsx
|
||||
packages/ui/src/theme/toggle.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/toggle.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)`, 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: 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 icon과 Fragment/element label 조합도 확인. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭으로 off → on → off(`aria-pressed`, `data-state` 일치), 키보드 focus 후 Enter로 on, Space로 off, disabled는 클릭과 focus 불가, controlled는 클릭으로 `pressed = true`와 `data-state="on"` 동기화.
|
||||
- computed style: default 34px / On 배경 `rgb(189,223,212)`·글자 `rgb(18,51,40)`, outline border `rgb(222,219,210)`, `sm` 28px, `lg` 42px, icon-only 34×34, disabled opacity 0.6, `className="w-40 rounded-full"` 적용(폭 160px, radius 최대).
|
||||
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, 390px(단일 버튼이라 폭 문제 없음).
|
||||
|
||||
## 14. 역할 구분 (Button · Checkbox · Radio Group · Switch와의 차이)
|
||||
|
||||
| 컴포넌트 | 역할 |
|
||||
|---|---|
|
||||
| Button | **Action 실행**(일회성). `variant`가 역할(primary/secondary/destructive…), `appearance`가 treatment |
|
||||
| Toggle | **pressed 상태 유지**(툴바식 on/off). `aria-pressed` |
|
||||
| Toggle Group | **선택 그룹**(단일/다중 선택, roving focus) |
|
||||
| Checkbox | 폼의 boolean / 다중 선택 |
|
||||
| Radio Group | 폼의 배타적 선택 |
|
||||
| Switch | 설정 on/off(즉시 적용되는 설정 값) |
|
||||
|
||||
Toggle은 Button과 모양이 비슷해 보여도 Button의 action 중심 variant 체계(role/appearance)와 `--hm-button-*` Token을 상속하지 않는다. 역할이 다르고, Button 규격이 바뀔 때 Toggle이 함께 바뀌면 안 되기 때문이다. 공통 의미(선택 면의 accent, control radius)만 Semantic으로 공유한다.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Tooltip
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tooltip 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Tooltip (Radix `Tooltip`) |
|
||||
| 대표 Story | `Components/Overlay/Tooltip` → `All` (이름 "Tooltip") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Tooltip 규격이 없다(인벤토리 "상세 동작·디자인 보류"). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary, text-inverse)로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Tooltip 구조(Provider, Root, Trigger, Content, Arrow)를 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Background | `--hm-sem-fg-primary` (진한 녹색) |
|
||||
| Text | `--hm-color-text-inverse` (White) |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Padding | 6px 10px |
|
||||
| Max width | 240px, 긴 문장은 줄바꿈(`text-balance`, `overflow-wrap: anywhere`) |
|
||||
| Typography | 12 / 18 / 400 |
|
||||
| Arrow | shadcn 회전 사각형, 면과 같은 색(배경·fill) |
|
||||
| Position | `sideOffset` 4, shadcn 기본 side(top) |
|
||||
| Delay | `TooltipProvider delayDuration` 기본 0 (shadcn 기본 유지) |
|
||||
|
||||
Tooltip은 짧고 즉각적인 설명만 담는다. 긴 설명·interactive 콘텐츠는 Popover / Hover Card를 쓴다. 그림자와 border는 없다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`(`delayed-open` 포함), shadcn open/close animation |
|
||||
| 열림 조건 | trigger hover 또는 keyboard focus |
|
||||
| 닫힘 조건 | pointer leave, blur, Escape, trigger 클릭 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Tooltip props 그대로. export: `TooltipProvider`, `Tooltip`, `TooltipTrigger`, `TooltipContent`.
|
||||
|
||||
- `Tooltip`은 상위 HM `TooltipProvider`가 없을 때만 자체 Provider를 만든다. 단독 사용은 그대로 지원하며 기본 `delayDuration`은 0이다.
|
||||
- 그룹에는 `@hm-design/ui`의 `TooltipProvider`를 둔다. 내부 Provider를 추가하지 않아 가장 가까운 Provider의 `delayDuration`, `skipDelayDuration`, `disableHoverableContent`와 그룹의 skip-delay 상태를 공유한다. Radix Provider를 직접 사용하는 대신 HM Provider를 사용한다.
|
||||
- 개별 `Tooltip delayDuration`은 Provider의 delay를 override한다. `skipDelayDuration`은 Provider에서 지정하며, skip 구간의 다음 Tooltip은 즉시 열린다. `skipDelayDuration={0}`이면 각 hover에 delay가 적용된다. keyboard focus는 delay 없이 열리고 Escape로 닫힌다.
|
||||
- `TooltipContent`: `sideOffset` 기본 4. 출력 속성: `data-slot="tooltip-*"`, Radix `data-state`·`data-side`. Arrow는 `data-slot="tooltip-arrow"`.
|
||||
|
||||
```tsx
|
||||
<TooltipProvider delayDuration={700} skipDelayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild><button>저장</button></TooltipTrigger>
|
||||
<TooltipContent>변경 사항을 저장합니다</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* 같은 Provider 아래의 Tooltip은 skip-delay 상태를 공유한다. */}
|
||||
</TooltipProvider>
|
||||
```
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/tooltip.css` (`--hm-tooltip-*`).
|
||||
|
||||
`background`, `text`, `radius`, `padding-x`, `padding-y`, `max-width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=tooltip`, `aria-describedby`)·Escape·hover/focus 동작, Provider 구조와 delay, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`, Arrow.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-foreground text-background rounded-md px-3 py-1.5 text-xs` 및 arrow의 `bg-foreground fill-foreground` class 제거 → theme CSS.
|
||||
- 최대 폭 240px 추가(shadcn은 `w-fit`만 있어 긴 문장 한계가 없었다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Tooltip"): 기본, 긴 텍스트, 비활성 버튼(wrapper trigger), 4방향 placement 비교.
|
||||
- verification: `Open`(열린 상태).
|
||||
- interactive control은 넣지 않는다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(240px, padding, 12px 글자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- delay: shadcn 기본 0 유지. HM delay 규칙은 없다.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
- 비활성 control에 붙이는 방식은 소비 조합(wrapper element).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/tooltip.tsx
|
||||
packages/ui/src/theme/tooltip.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/tooltip.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08: 공개 패키지 빌드·타입 검사 통과. Browser에서 mouse `PointerEvent`로 단독 사용, 상위/중첩 HM Provider delay, Root override, 그룹 skip-delay·만료·0 설정과 pointer leave를 검증했다. 실제 키보드로 focus·blur·Escape, controlled `open`/`onOpenChange`, `asChild`·ref, 비활성 wrapper를 확인했다. 주요 computed style은 수정 전과 동일하다. 실제 마우스 이동을 통한 hover는 별도 미검증이다.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): keyboard focus로 열림(`role=tooltip` 확인), Escape 닫힘.
|
||||
- computed style: bg `rgb(10,34,29)`, text white, radius 4px, padding 6px 10px, 12/18, arrow fill 같은 색.
|
||||
- 미확인: 실제 pointer hover 열림·pointer leave 닫힘, 긴 텍스트 줄바꿈 시각, 4방향 placement 시각, 비활성 wrapper trigger.
|
||||
Reference in new issue
Block a user