@ilokesto/toast 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +209 -0
- package/README.md +209 -0
- package/dist/components/ToastBar.d.ts +12 -0
- package/dist/components/ToastBar.js +39 -0
- package/dist/components/ToastProvider.d.ts +3 -0
- package/dist/components/ToastProvider.js +16 -0
- package/dist/components/Toaster.d.ts +2 -0
- package/dist/components/Toaster.js +175 -0
- package/dist/components/icons.d.ts +14 -0
- package/dist/components/icons.js +83 -0
- package/dist/core/createToastRuntime.d.ts +2 -0
- package/dist/core/createToastRuntime.js +247 -0
- package/dist/core/createToastStore.d.ts +2 -0
- package/dist/core/createToastStore.js +126 -0
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +18 -0
- package/dist/core/toast.d.ts +2 -0
- package/dist/core/toast.js +41 -0
- package/dist/core/utils.d.ts +11 -0
- package/dist/core/utils.js +33 -0
- package/dist/hooks/useToastItems.d.ts +2 -0
- package/dist/hooks/useToastItems.js +4 -0
- package/dist/hooks/useToaster.d.ts +2 -0
- package/dist/hooks/useToaster.js +31 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/types/toast.d.ts +137 -0
- package/dist/types/toast.js +1 -0
- package/package.json +50 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @ilokesto/toast
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | **한국어**
|
|
4
|
+
|
|
5
|
+
실사용 기준으로 `react-hot-toast`를 대체할 수 있도록 만든 React toast 패키지입니다.
|
|
6
|
+
|
|
7
|
+
`@ilokesto/toast`는 provider-scoped toast runtime, 익숙한 `toast.*` facade, 기본 렌더러, headless hook, 그리고 선택적인 top-layer transport를 제공합니다. 내부적으로는 `@ilokesto/overlay`를 presence lifecycle 용도로만 사용하고, toast 정책과 렌더링은 이 패키지 안에서 처리합니다.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 익숙한 facade: `toast()`, `toast.success()`, `toast.error()`, `toast.loading()`, `toast.custom()`, `toast.promise()`
|
|
12
|
+
- `toasterId` 기준으로 분리되는 provider-scoped runtime
|
|
13
|
+
- 기본 `Toaster`에 포함된 카드 UI, 상태 아이콘, spinner, enter/exit motion
|
|
14
|
+
- `style`, `className`, `icon`, `iconTheme`, `removeDelay`, `position`, `duration`, `ariaProps` 같은 per-toast 옵션
|
|
15
|
+
- global → per-type → per-toast 순서의 `toastOptions` merge
|
|
16
|
+
- 커스텀 렌더러를 위한 headless `useToaster()`
|
|
17
|
+
- manual popover 기반의 선택적 `transport="top-layer"`
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @ilokesto/toast react react-dom
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
또는
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @ilokesto/toast react react-dom
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Toaster, toast } from '@ilokesto/toast';
|
|
35
|
+
|
|
36
|
+
function App() {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<button onClick={() => toast.success('Saved successfully')}>Show toast</button>
|
|
40
|
+
<Toaster />
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Promise Toasts
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Toaster, toast } from '@ilokesto/toast';
|
|
50
|
+
|
|
51
|
+
async function savePost() {
|
|
52
|
+
return fetch('/api/posts', { method: 'POST' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toast.promise(savePost(), {
|
|
56
|
+
loading: 'Saving post…',
|
|
57
|
+
success: 'Post saved',
|
|
58
|
+
error: (error) => `Save failed: ${String(error)}`,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function App() {
|
|
62
|
+
return <Toaster />;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Custom Rendering
|
|
67
|
+
|
|
68
|
+
### `Toaster` children
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Toaster } from '@ilokesto/toast';
|
|
72
|
+
|
|
73
|
+
export function App() {
|
|
74
|
+
return (
|
|
75
|
+
<Toaster>
|
|
76
|
+
{(toast, { dismiss }) => (
|
|
77
|
+
<button onClick={dismiss}>
|
|
78
|
+
{toast.message}
|
|
79
|
+
</button>
|
|
80
|
+
)}
|
|
81
|
+
</Toaster>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `ToastBar`
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { ToastBar, Toaster } from '@ilokesto/toast';
|
|
90
|
+
|
|
91
|
+
export function App() {
|
|
92
|
+
return (
|
|
93
|
+
<Toaster>
|
|
94
|
+
{(toast) => (
|
|
95
|
+
<ToastBar toast={toast}>
|
|
96
|
+
{({ icon, message }) => (
|
|
97
|
+
<>
|
|
98
|
+
{message}
|
|
99
|
+
{icon}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</ToastBar>
|
|
103
|
+
)}
|
|
104
|
+
</Toaster>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Top-Layer Transport
|
|
110
|
+
|
|
111
|
+
`@ilokesto/toast`는 `react-hot-toast`에 없는 추가 렌더링 모드를 제공합니다.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<Toaster transport="top-layer" />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
브라우저가 지원하면 manual popover를 사용하고, 지원하지 않으면 inline 렌더링으로 자동 fallback합니다.
|
|
118
|
+
|
|
119
|
+
## Headless Usage
|
|
120
|
+
|
|
121
|
+
`useToaster()`는 visible toast 목록과 runtime helper를 노출합니다.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const { toasts, handlers } = useToaster();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
사용 가능한 handler:
|
|
128
|
+
|
|
129
|
+
- `updateHeight`
|
|
130
|
+
- `startPause`
|
|
131
|
+
- `endPause`
|
|
132
|
+
- `calculateOffset`
|
|
133
|
+
|
|
134
|
+
이 hook은 mounted `Toaster` context 아래에서 동작하는 고급 통합 시나리오를 위한 API입니다.
|
|
135
|
+
|
|
136
|
+
## Source Layout
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
src/
|
|
140
|
+
components/
|
|
141
|
+
ToastBar.tsx
|
|
142
|
+
ToastProvider.tsx
|
|
143
|
+
Toaster.tsx
|
|
144
|
+
icons.tsx
|
|
145
|
+
core/
|
|
146
|
+
createToastRuntime.ts
|
|
147
|
+
createToastStore.ts
|
|
148
|
+
registry.ts
|
|
149
|
+
toast.ts
|
|
150
|
+
utils.ts
|
|
151
|
+
hooks/
|
|
152
|
+
useToaster.ts
|
|
153
|
+
useToastItems.ts
|
|
154
|
+
types/
|
|
155
|
+
toast.ts
|
|
156
|
+
index.ts
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Responsibilities
|
|
160
|
+
|
|
161
|
+
### `src/components`
|
|
162
|
+
|
|
163
|
+
- `Toaster.tsx` → visible toast stack을 마운트하고 runtime view 옵션을 설정하며 기본 컨테이너를 제공합니다
|
|
164
|
+
- `ToastBar.tsx` → 개별 toast row를 위한 기본 렌더러 primitive입니다
|
|
165
|
+
- `icons.tsx` → 기본 spinner와 상태 아이콘 컴포넌트를 제공합니다
|
|
166
|
+
- `ToastProvider.tsx` → `toasterId`별 provider-scoped runtime을 만들고 등록합니다
|
|
167
|
+
|
|
168
|
+
### `src/core`
|
|
169
|
+
|
|
170
|
+
- `toast.ts` → public `toast.*` facade
|
|
171
|
+
- `createToastRuntime.ts` → id, timer, promise transition, dismiss/remove, visible-set 계산을 담당하는 toast policy 레이어
|
|
172
|
+
- `createToastStore.ts` → raw toast state store
|
|
173
|
+
- `registry.ts` → `toasterId` 기준 runtime 등록
|
|
174
|
+
- `utils.ts` → 기본 duration, id, icon theme, helper 정의
|
|
175
|
+
|
|
176
|
+
### `src/hooks`
|
|
177
|
+
|
|
178
|
+
- `useToaster.ts` → `{ toasts, handlers }`를 반환하는 headless hook
|
|
179
|
+
- `useToastItems.ts` → 현재 visible toast item 목록을 구독합니다
|
|
180
|
+
|
|
181
|
+
### `src/types`
|
|
182
|
+
|
|
183
|
+
- `toast.ts` → runtime API, props, item state, options에 대한 public contract
|
|
184
|
+
|
|
185
|
+
### `src/index.ts`
|
|
186
|
+
|
|
187
|
+
- public runtime, renderer, hook, type surface를 다시 export합니다
|
|
188
|
+
|
|
189
|
+
## Exports
|
|
190
|
+
|
|
191
|
+
- values → `toast`, `Toaster`, `ToastBar`, `ToastIcon`, `useToaster`, `useToastItems`, `createToastRuntime`
|
|
192
|
+
- types → `ToastBarProps`, `ToastOptions`, `DefaultToastOptions`, `ToasterProps`, `UseToasterResult` 등 toast 관련 public 타입
|
|
193
|
+
|
|
194
|
+
## Migration
|
|
195
|
+
|
|
196
|
+
`react-hot-toast`에서 옮겨오는 경우 [MIGRATION_FROM_REACT_HOT_TOAST.ko.md](./MIGRATION_FROM_REACT_HOT_TOAST.ko.md)를 참고하세요.
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pnpm install
|
|
202
|
+
pnpm run build
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
빌드 결과물은 `dist` 디렉터리에 생성됩니다.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @ilokesto/toast
|
|
2
|
+
|
|
3
|
+
**English** | [한국어](./README.ko.md)
|
|
4
|
+
|
|
5
|
+
A React toast package built for practical `react-hot-toast` replacement quality.
|
|
6
|
+
|
|
7
|
+
`@ilokesto/toast` provides a provider-scoped toast runtime, a familiar `toast.*` facade, a polished default renderer, headless hooks, and an optional top-layer transport. Internally it uses `@ilokesto/overlay` for presence lifecycle only, while toast policy and rendering stay inside this package.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Familiar facade: `toast()`, `toast.success()`, `toast.error()`, `toast.loading()`, `toast.custom()`, `toast.promise()`
|
|
12
|
+
- Provider-scoped runtimes separated by `toasterId`
|
|
13
|
+
- Default `Toaster` with built-in card UI, animated status icons, spinner, and enter/exit motion
|
|
14
|
+
- Per-toast options such as `style`, `className`, `icon`, `iconTheme`, `removeDelay`, `position`, `duration`, and `ariaProps`
|
|
15
|
+
- `toastOptions` merge order of global → per-type → per-toast options
|
|
16
|
+
- Headless `useToaster()` for custom renderers
|
|
17
|
+
- Optional `transport="top-layer"` mode powered by manual popover
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @ilokesto/toast react react-dom
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
or
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @ilokesto/toast react react-dom
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Toaster, toast } from '@ilokesto/toast';
|
|
35
|
+
|
|
36
|
+
function App() {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<button onClick={() => toast.success('Saved successfully')}>Show toast</button>
|
|
40
|
+
<Toaster />
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Promise Toasts
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Toaster, toast } from '@ilokesto/toast';
|
|
50
|
+
|
|
51
|
+
async function savePost() {
|
|
52
|
+
return fetch('/api/posts', { method: 'POST' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toast.promise(savePost(), {
|
|
56
|
+
loading: 'Saving post…',
|
|
57
|
+
success: 'Post saved',
|
|
58
|
+
error: (error) => `Save failed: ${String(error)}`,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function App() {
|
|
62
|
+
return <Toaster />;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Custom Rendering
|
|
67
|
+
|
|
68
|
+
### `Toaster` children
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Toaster } from '@ilokesto/toast';
|
|
72
|
+
|
|
73
|
+
export function App() {
|
|
74
|
+
return (
|
|
75
|
+
<Toaster>
|
|
76
|
+
{(toast, { dismiss }) => (
|
|
77
|
+
<button onClick={dismiss}>
|
|
78
|
+
{toast.message}
|
|
79
|
+
</button>
|
|
80
|
+
)}
|
|
81
|
+
</Toaster>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `ToastBar`
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { ToastBar, Toaster } from '@ilokesto/toast';
|
|
90
|
+
|
|
91
|
+
export function App() {
|
|
92
|
+
return (
|
|
93
|
+
<Toaster>
|
|
94
|
+
{(toast) => (
|
|
95
|
+
<ToastBar toast={toast}>
|
|
96
|
+
{({ icon, message }) => (
|
|
97
|
+
<>
|
|
98
|
+
{message}
|
|
99
|
+
{icon}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</ToastBar>
|
|
103
|
+
)}
|
|
104
|
+
</Toaster>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Top-Layer Transport
|
|
110
|
+
|
|
111
|
+
`@ilokesto/toast` adds an extra rendering mode that `react-hot-toast` does not provide:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<Toaster transport="top-layer" />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This uses manual popover when the browser supports it and falls back to inline rendering when it does not.
|
|
118
|
+
|
|
119
|
+
## Headless Usage
|
|
120
|
+
|
|
121
|
+
`useToaster()` exposes the visible toast list and runtime helpers:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const { toasts, handlers } = useToaster();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Available handlers:
|
|
128
|
+
|
|
129
|
+
- `updateHeight`
|
|
130
|
+
- `startPause`
|
|
131
|
+
- `endPause`
|
|
132
|
+
- `calculateOffset`
|
|
133
|
+
|
|
134
|
+
This hook is intended for advanced integrations that run under the mounted `Toaster` context.
|
|
135
|
+
|
|
136
|
+
## Source Layout
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
src/
|
|
140
|
+
components/
|
|
141
|
+
ToastBar.tsx
|
|
142
|
+
ToastProvider.tsx
|
|
143
|
+
Toaster.tsx
|
|
144
|
+
icons.tsx
|
|
145
|
+
core/
|
|
146
|
+
createToastRuntime.ts
|
|
147
|
+
createToastStore.ts
|
|
148
|
+
registry.ts
|
|
149
|
+
toast.ts
|
|
150
|
+
utils.ts
|
|
151
|
+
hooks/
|
|
152
|
+
useToaster.ts
|
|
153
|
+
useToastItems.ts
|
|
154
|
+
types/
|
|
155
|
+
toast.ts
|
|
156
|
+
index.ts
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Responsibilities
|
|
160
|
+
|
|
161
|
+
### `src/components`
|
|
162
|
+
|
|
163
|
+
- `Toaster.tsx` → mounts the visible toast stack, configures runtime view options, and provides the default container
|
|
164
|
+
- `ToastBar.tsx` → composable default renderer primitive for a single toast row
|
|
165
|
+
- `icons.tsx` → built-in spinner and status icon components used by the default renderer
|
|
166
|
+
- `ToastProvider.tsx` → creates and registers provider-scoped runtimes by `toasterId`
|
|
167
|
+
|
|
168
|
+
### `src/core`
|
|
169
|
+
|
|
170
|
+
- `toast.ts` → the public `toast.*` facade
|
|
171
|
+
- `createToastRuntime.ts` → toast policy layer for ids, timers, promise transitions, dismiss/remove behavior, and visible-set calculation
|
|
172
|
+
- `createToastStore.ts` → raw toast state store
|
|
173
|
+
- `registry.ts` → runtime registration by `toasterId`
|
|
174
|
+
- `utils.ts` → default durations, ids, icon themes, and small helpers
|
|
175
|
+
|
|
176
|
+
### `src/hooks`
|
|
177
|
+
|
|
178
|
+
- `useToaster.ts` → headless hook returning `{ toasts, handlers }`
|
|
179
|
+
- `useToastItems.ts` → subscribes to the current visible toast items
|
|
180
|
+
|
|
181
|
+
### `src/types`
|
|
182
|
+
|
|
183
|
+
- `toast.ts` → public contracts for runtime APIs, props, item state, and options
|
|
184
|
+
|
|
185
|
+
### `src/index.ts`
|
|
186
|
+
|
|
187
|
+
- re-exports the public runtime, renderer, hook, and type surface
|
|
188
|
+
|
|
189
|
+
## Exports
|
|
190
|
+
|
|
191
|
+
- values → `toast`, `Toaster`, `ToastBar`, `ToastIcon`, `useToaster`, `useToastItems`, `createToastRuntime`
|
|
192
|
+
- types → `ToastBarProps`, `ToastOptions`, `DefaultToastOptions`, `ToasterProps`, `UseToasterResult`, and related toast contracts
|
|
193
|
+
|
|
194
|
+
## Migration
|
|
195
|
+
|
|
196
|
+
If you are moving from `react-hot-toast`, see [MIGRATION_FROM_REACT_HOT_TOAST.md](./MIGRATION_FROM_REACT_HOT_TOAST.md).
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pnpm install
|
|
202
|
+
pnpm run build
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Build outputs are generated in the `dist` directory.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { ToastItem, ToastPosition } from "../types/toast";
|
|
3
|
+
export interface ToastBarProps {
|
|
4
|
+
readonly toast: ToastItem;
|
|
5
|
+
readonly position?: ToastPosition;
|
|
6
|
+
readonly style?: CSSProperties;
|
|
7
|
+
readonly children?: (components: {
|
|
8
|
+
icon: ReactNode;
|
|
9
|
+
message: ReactNode;
|
|
10
|
+
}) => ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare const ToastBar: ({ toast, position, style, children }: ToastBarProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ToastIcon } from "./icons";
|
|
3
|
+
const getMotionStyle = (toast, position) => {
|
|
4
|
+
const isClosing = toast.status === "closing";
|
|
5
|
+
const factor = position.startsWith("top") ? -1 : 1;
|
|
6
|
+
const isCenter = position.endsWith("center");
|
|
7
|
+
const enterAnimation = `toast-enter 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards`;
|
|
8
|
+
const exitAnimation = `toast-exit 0.4s forwards cubic-bezier(0.06, 0.71, 0.55, 1)`;
|
|
9
|
+
return {
|
|
10
|
+
animation: isClosing ? exitAnimation : enterAnimation,
|
|
11
|
+
"--toast-enter-y": `${factor * 20}px`,
|
|
12
|
+
"--toast-exit-y": `${factor * -20}px`,
|
|
13
|
+
"--toast-enter-x": isCenter ? "0px" : position.endsWith("left") ? "-20px" : "20px",
|
|
14
|
+
"--toast-exit-x": isCenter ? "0px" : position.endsWith("left") ? "-20px" : "20px",
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export const ToastBar = ({ toast, position, style, children }) => {
|
|
18
|
+
const isCustom = toast.type === "custom";
|
|
19
|
+
const defaultStyle = {
|
|
20
|
+
display: "flex",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
gap: 8,
|
|
23
|
+
background: "#fff",
|
|
24
|
+
color: "#363636",
|
|
25
|
+
lineHeight: 1.3,
|
|
26
|
+
willChange: "transform, opacity",
|
|
27
|
+
boxShadow: "0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05)",
|
|
28
|
+
maxWidth: 350,
|
|
29
|
+
pointerEvents: "auto",
|
|
30
|
+
padding: "8px 10px",
|
|
31
|
+
borderRadius: 8,
|
|
32
|
+
...getMotionStyle(toast, position ?? toast.position ?? "top-center"),
|
|
33
|
+
...toast.style,
|
|
34
|
+
...style,
|
|
35
|
+
};
|
|
36
|
+
const icon = _jsx(ToastIcon, { type: toast.type, icon: toast.icon, iconTheme: toast.iconTheme });
|
|
37
|
+
const message = (_jsx("div", { style: { display: "flex", justifyContent: "center", margin: "4px 10px", color: "inherit", flex: 1 }, children: toast.message }));
|
|
38
|
+
return (_jsx("div", { className: toast.className, style: defaultStyle, role: toast.ariaProps.role, "aria-live": toast.ariaProps["aria-live"], "aria-atomic": toast.ariaProps["aria-atomic"], children: isCustom ? (toast.message) : typeof children === "function" ? (children({ icon, message })) : (_jsxs(_Fragment, { children: [icon, message] })) }));
|
|
39
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useEffect, useMemo } from "react";
|
|
2
|
+
import { createToastRuntime } from "../core/createToastRuntime";
|
|
3
|
+
import { registerRuntime, unregisterRuntime } from "../core/registry";
|
|
4
|
+
import { DEFAULT_TOASTER_ID } from "../core/utils";
|
|
5
|
+
export const ToasterContext = createContext(null);
|
|
6
|
+
export function useToasterRuntime(toasterId = DEFAULT_TOASTER_ID) {
|
|
7
|
+
const runtime = useMemo(() => createToastRuntime(toasterId), [toasterId]);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
registerRuntime(runtime);
|
|
10
|
+
return () => {
|
|
11
|
+
runtime.clear();
|
|
12
|
+
unregisterRuntime(runtime.toasterId);
|
|
13
|
+
};
|
|
14
|
+
}, [runtime]);
|
|
15
|
+
return runtime;
|
|
16
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ToasterProps } from "../types/toast";
|
|
2
|
+
export declare function Toaster({ toasterId, position, transport, limit, reverseOrder, gutter, containerStyle, containerClassName, toastOptions, children: renderRow, }: ToasterProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, } from "react";
|
|
3
|
+
import { DEFAULT_GUTTER, DEFAULT_LIMIT, DEFAULT_POSITION } from "../core/utils";
|
|
4
|
+
import { useToastItems } from "../hooks/useToastItems";
|
|
5
|
+
import { ToastBar } from "./ToastBar";
|
|
6
|
+
import { ToasterContext, useToasterRuntime } from "./ToastProvider";
|
|
7
|
+
function supportsPopover() {
|
|
8
|
+
if (typeof HTMLElement === "undefined") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return typeof HTMLElement.prototype.showPopover === "function";
|
|
12
|
+
}
|
|
13
|
+
function getContainerStyle(position) {
|
|
14
|
+
const style = {
|
|
15
|
+
position: "fixed",
|
|
16
|
+
zIndex: 9999,
|
|
17
|
+
display: "flex",
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
pointerEvents: "none",
|
|
20
|
+
padding: 16,
|
|
21
|
+
};
|
|
22
|
+
if (position.startsWith("top")) {
|
|
23
|
+
style.top = 0;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
style.bottom = 0;
|
|
27
|
+
}
|
|
28
|
+
if (position.endsWith("left")) {
|
|
29
|
+
style.left = 0;
|
|
30
|
+
style.alignItems = "flex-start";
|
|
31
|
+
}
|
|
32
|
+
else if (position.endsWith("right")) {
|
|
33
|
+
style.right = 0;
|
|
34
|
+
style.alignItems = "flex-end";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
style.left = "50%";
|
|
38
|
+
style.transform = "translateX(-50%)";
|
|
39
|
+
style.alignItems = "center";
|
|
40
|
+
}
|
|
41
|
+
return style;
|
|
42
|
+
}
|
|
43
|
+
function getStackOffset(items, index, gutter) {
|
|
44
|
+
return items.slice(0, index).reduce((offset, item) => {
|
|
45
|
+
return offset + (item.height ?? 0) + gutter;
|
|
46
|
+
}, 0);
|
|
47
|
+
}
|
|
48
|
+
function getToastStyle(position, offset) {
|
|
49
|
+
const translateY = position.startsWith("top") ? offset : -offset;
|
|
50
|
+
return {
|
|
51
|
+
pointerEvents: "auto",
|
|
52
|
+
transform: `translateY(${translateY}px)`,
|
|
53
|
+
transition: "transform 200ms ease, opacity 200ms ease",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function ToastMeasure({ item, offset, position, onHeight, children, }) {
|
|
57
|
+
const ref = useRef(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const element = ref.current;
|
|
60
|
+
if (element === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const nextHeight = element.offsetHeight;
|
|
64
|
+
onHeight(item.id, nextHeight);
|
|
65
|
+
}, [item.id, item.message, item.status, onHeight]);
|
|
66
|
+
return (_jsx("div", { ref: ref, style: getToastStyle(position, offset), children: children }));
|
|
67
|
+
}
|
|
68
|
+
function TopLayerContainer({ style, className, children, }) {
|
|
69
|
+
const ref = useRef(null);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const element = ref.current;
|
|
72
|
+
if (element === null || !supportsPopover()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
element.showPopover();
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error("Failed to show top-layer toast popover", error);
|
|
80
|
+
}
|
|
81
|
+
return () => {
|
|
82
|
+
if (typeof element.hidePopover !== "function") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
element.hidePopover();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error("Failed to hide top-layer toast popover", error);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
if (!supportsPopover()) {
|
|
94
|
+
return (_jsx("div", { className: className, style: style, children: children }));
|
|
95
|
+
}
|
|
96
|
+
return (_jsx("div", { ref: ref, popover: "manual", className: className, style: {
|
|
97
|
+
...style,
|
|
98
|
+
border: "none",
|
|
99
|
+
background: "transparent",
|
|
100
|
+
margin: 0,
|
|
101
|
+
overflow: "visible",
|
|
102
|
+
inset: "unset",
|
|
103
|
+
}, children: children }));
|
|
104
|
+
}
|
|
105
|
+
function InlineContainer({ style, className, children, }) {
|
|
106
|
+
return (_jsx("div", { className: className, style: style, children: children }));
|
|
107
|
+
}
|
|
108
|
+
export function Toaster({ toasterId, position = DEFAULT_POSITION, transport = "inline", limit = DEFAULT_LIMIT, reverseOrder = false, gutter = DEFAULT_GUTTER, containerStyle, containerClassName, toastOptions, children: renderRow, }) {
|
|
109
|
+
const runtime = useToasterRuntime(toasterId);
|
|
110
|
+
const items = useToastItems(runtime);
|
|
111
|
+
const activePosition = toastOptions?.position ?? position;
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
runtime.configureView({
|
|
114
|
+
limit,
|
|
115
|
+
position: activePosition,
|
|
116
|
+
toastOptions,
|
|
117
|
+
});
|
|
118
|
+
}, [activePosition, limit, runtime, toastOptions]);
|
|
119
|
+
const orderedItems = useMemo(() => {
|
|
120
|
+
return reverseOrder ? [...items].reverse() : items;
|
|
121
|
+
}, [items, reverseOrder]);
|
|
122
|
+
const containerStyleValue = useMemo(() => ({
|
|
123
|
+
...getContainerStyle(position),
|
|
124
|
+
...containerStyle,
|
|
125
|
+
}), [containerStyle, position]);
|
|
126
|
+
const createHelpers = useCallback((item) => ({
|
|
127
|
+
dismiss: () => runtime.dismiss(item.id),
|
|
128
|
+
remove: () => runtime.remove(item.id),
|
|
129
|
+
}), [runtime]);
|
|
130
|
+
const handleMouseEnter = useCallback(() => {
|
|
131
|
+
runtime.startPause();
|
|
132
|
+
}, [runtime]);
|
|
133
|
+
const handleMouseLeave = useCallback(() => {
|
|
134
|
+
runtime.endPause();
|
|
135
|
+
}, [runtime]);
|
|
136
|
+
const rows = orderedItems.map((item, index) => {
|
|
137
|
+
const helpers = createHelpers(item);
|
|
138
|
+
const offset = getStackOffset(orderedItems, index, gutter);
|
|
139
|
+
const content = renderRow === undefined
|
|
140
|
+
? _jsx(ToastBar, { toast: item, position: position })
|
|
141
|
+
: renderRow(item, helpers);
|
|
142
|
+
return (_jsx(ToastMeasure, { item: item, offset: offset, position: position, onHeight: runtime.updateHeight, children: content }, item.id));
|
|
143
|
+
});
|
|
144
|
+
const Container = transport === "top-layer" ? TopLayerContainer : InlineContainer;
|
|
145
|
+
return (_jsxs(ToasterContext.Provider, { value: runtime, children: [_jsx("style", { children: `
|
|
146
|
+
@keyframes toast-enter {
|
|
147
|
+
0% { transform: translate3d(var(--toast-enter-x, 0), var(--toast-enter-y, -200%), 0) scale(0.6); opacity: 0.5; }
|
|
148
|
+
100% { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
|
|
149
|
+
}
|
|
150
|
+
@keyframes toast-exit {
|
|
151
|
+
0% { transform: translate3d(0, 0, -1px) scale(1); opacity: 1; }
|
|
152
|
+
100% { transform: translate3d(var(--toast-exit-x, 0), var(--toast-exit-y, -150%), -1px) scale(0.6); opacity: 0; }
|
|
153
|
+
}
|
|
154
|
+
@keyframes toast-spin {
|
|
155
|
+
from { transform: rotate(0deg); }
|
|
156
|
+
to { transform: rotate(360deg); }
|
|
157
|
+
}
|
|
158
|
+
@keyframes toast-scale {
|
|
159
|
+
from { transform: scale(0); opacity: 0; }
|
|
160
|
+
to { transform: scale(1); opacity: 1; }
|
|
161
|
+
}
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
@keyframes toast-enter {
|
|
164
|
+
0% { opacity: 0; }
|
|
165
|
+
100% { opacity: 1; }
|
|
166
|
+
}
|
|
167
|
+
@keyframes toast-exit {
|
|
168
|
+
0% { opacity: 1; }
|
|
169
|
+
100% { opacity: 0; }
|
|
170
|
+
}
|
|
171
|
+
.toast-motion-spin { animation: none !important; }
|
|
172
|
+
.toast-motion-scale { animation: none !important; }
|
|
173
|
+
}
|
|
174
|
+
` }), _jsx(Container, { className: containerClassName, style: containerStyleValue, children: _jsx("div", { role: "region", "aria-label": "Notifications", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: rows }) })] }));
|
|
175
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ToastType, IconTheme } from "../types/toast";
|
|
3
|
+
export declare const DefaultSpinner: () => import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export declare const DefaultSuccess: ({ theme }: {
|
|
5
|
+
theme?: IconTheme;
|
|
6
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare const DefaultError: ({ theme }: {
|
|
8
|
+
theme?: IconTheme;
|
|
9
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare const ToastIcon: ({ type, icon, iconTheme, }: {
|
|
11
|
+
type: ToastType;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
iconTheme?: IconTheme;
|
|
14
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|