Files

159 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dialog
> 2026-10-10 추가 수정 요청 기준: 일반 확인과 삭제 확인의 취소·실행은 모두 Standard Solid. 과거 검증 기록은 당시 결과이며 현재 검증은 [추가 수정 보고서](../guidelines/publisher-additional-review-2026-10-10.md)를 따릅니다.
현재 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 대체). 이는 당시 ui_viz 근거다. 2026-10-14 HM 정책은 400 / 500 / 700이며 카드 제목도 700을 사용한다. Dialog Title의 700은 유지한다.
- **현재 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`+`solid` / 삭제 `destructive`+`solid`. 같은 세트의 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는 Solid와 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 안에 둔 조합.