Files
HMDesign/docs/components/sonner.md

121 lines
8.0 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.
# 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 모두 폭 안에서 줄바꿈.