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