# 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에 한 번: 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--*`를 추가한다. - 수치(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 모두 폭 안에서 줄바꿈.