@hanzogui/toast 3.0.2 → 7.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/package.json +42 -25
- package/src/Toast.tsx +5 -5
- package/src/ToastAnnounce.tsx +2 -2
- package/src/ToastComposable.tsx +1380 -0
- package/src/ToastImpl.tsx +19 -7
- package/src/ToastItemFrame.tsx +136 -0
- package/src/ToastPortal.tsx +2 -2
- package/src/ToastProvider.tsx +13 -6
- package/src/ToastState.ts +398 -0
- package/src/ToastViewport.tsx +11 -10
- package/src/Toaster.tsx +181 -0
- package/src/createNativeToast.native.tsx +4 -0
- package/src/createNativeToast.tsx +18 -24
- package/src/dispatchNativeToast.ts +43 -0
- package/src/useAnimatedDragGesture.native.ts +255 -0
- package/src/useAnimatedDragGesture.ts +319 -0
- package/src/useReducedMotion.ts +59 -0
- package/src/useToastAnimations.ts +377 -0
- package/src/v2.ts +31 -0
- package/types/Toast.d.ts +36 -36
- package/types/ToastAnnounce.d.ts +4 -4
- package/types/ToastComposable.d.ts +199 -0
- package/types/ToastImperative.d.ts +2 -2
- package/types/ToastImpl.d.ts +12 -12
- package/types/ToastItemFrame.d.ts +25 -0
- package/types/ToastProvider.d.ts +8 -8
- package/types/ToastState.d.ts +179 -0
- package/types/ToastViewport.d.ts +12 -12
- package/types/Toaster.d.ts +112 -0
- package/types/createNativeToast.d.ts +5 -0
- package/types/createNativeToast.native.d.ts +1 -0
- package/types/dispatchNativeToast.d.ts +12 -0
- package/types/useAnimatedDragGesture.d.ts +33 -0
- package/types/useAnimatedDragGesture.native.d.ts +33 -0
- package/types/useReducedMotion.d.ts +6 -0
- package/types/useToastAnimations.d.ts +50 -0
- package/types/v2.d.ts +10 -0
- package/v2/index.cjs +2 -0
- package/v2/index.js +2 -0
- package/v2/index.native.cjs +2 -0
- package/v2/index.native.js +2 -0
- package/LICENSE +0 -21
- package/dist/cjs/Toast.cjs +0 -170
- package/dist/cjs/Toast.native.js +0 -174
- package/dist/cjs/Toast.native.js.map +0 -1
- package/dist/cjs/ToastAnnounce.cjs +0 -97
- package/dist/cjs/ToastAnnounce.native.js +0 -105
- package/dist/cjs/ToastAnnounce.native.js.map +0 -1
- package/dist/cjs/ToastImperative.cjs +0 -100
- package/dist/cjs/ToastImperative.native.js +0 -122
- package/dist/cjs/ToastImperative.native.js.map +0 -1
- package/dist/cjs/ToastImpl.cjs +0 -292
- package/dist/cjs/ToastImpl.native.js +0 -326
- package/dist/cjs/ToastImpl.native.js.map +0 -1
- package/dist/cjs/ToastPortal.cjs +0 -44
- package/dist/cjs/ToastPortal.native.js +0 -47
- package/dist/cjs/ToastPortal.native.js.map +0 -1
- package/dist/cjs/ToastProvider.cjs +0 -146
- package/dist/cjs/ToastProvider.native.js +0 -159
- package/dist/cjs/ToastProvider.native.js.map +0 -1
- package/dist/cjs/ToastViewport.cjs +0 -278
- package/dist/cjs/ToastViewport.native.js +0 -315
- package/dist/cjs/ToastViewport.native.js.map +0 -1
- package/dist/cjs/constants.cjs +0 -28
- package/dist/cjs/constants.native.js +0 -31
- package/dist/cjs/constants.native.js.map +0 -1
- package/dist/cjs/createNativeToast.cjs +0 -51
- package/dist/cjs/createNativeToast.native.js +0 -48
- package/dist/cjs/createNativeToast.native.js.map +0 -1
- package/dist/cjs/index.cjs +0 -18
- package/dist/cjs/index.native.js +0 -21
- package/dist/cjs/index.native.js.map +0 -1
- package/dist/cjs/types.cjs +0 -16
- package/dist/cjs/types.native.js +0 -19
- package/dist/cjs/types.native.js.map +0 -1
- package/dist/esm/Toast.mjs +0 -131
- package/dist/esm/Toast.mjs.map +0 -1
- package/dist/esm/Toast.native.js +0 -132
- package/dist/esm/Toast.native.js.map +0 -1
- package/dist/esm/ToastAnnounce.mjs +0 -62
- package/dist/esm/ToastAnnounce.mjs.map +0 -1
- package/dist/esm/ToastAnnounce.native.js +0 -67
- package/dist/esm/ToastAnnounce.native.js.map +0 -1
- package/dist/esm/ToastImperative.mjs +0 -63
- package/dist/esm/ToastImperative.mjs.map +0 -1
- package/dist/esm/ToastImperative.native.js +0 -82
- package/dist/esm/ToastImperative.native.js.map +0 -1
- package/dist/esm/ToastImpl.mjs +0 -256
- package/dist/esm/ToastImpl.mjs.map +0 -1
- package/dist/esm/ToastImpl.native.js +0 -287
- package/dist/esm/ToastImpl.native.js.map +0 -1
- package/dist/esm/ToastPortal.mjs +0 -21
- package/dist/esm/ToastPortal.mjs.map +0 -1
- package/dist/esm/ToastPortal.native.js +0 -21
- package/dist/esm/ToastPortal.native.js.map +0 -1
- package/dist/esm/ToastProvider.mjs +0 -108
- package/dist/esm/ToastProvider.mjs.map +0 -1
- package/dist/esm/ToastProvider.native.js +0 -118
- package/dist/esm/ToastProvider.native.js.map +0 -1
- package/dist/esm/ToastViewport.mjs +0 -241
- package/dist/esm/ToastViewport.mjs.map +0 -1
- package/dist/esm/ToastViewport.native.js +0 -275
- package/dist/esm/ToastViewport.native.js.map +0 -1
- package/dist/esm/constants.mjs +0 -4
- package/dist/esm/constants.mjs.map +0 -1
- package/dist/esm/constants.native.js +0 -4
- package/dist/esm/constants.native.js.map +0 -1
- package/dist/esm/createNativeToast.mjs +0 -27
- package/dist/esm/createNativeToast.mjs.map +0 -1
- package/dist/esm/createNativeToast.native.js +0 -21
- package/dist/esm/createNativeToast.native.js.map +0 -1
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs +0 -2
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js +0 -2
- package/dist/esm/index.native.js.map +0 -1
- package/dist/esm/types.mjs +0 -2
- package/dist/esm/types.mjs.map +0 -1
- package/dist/esm/types.native.js +0 -2
- package/dist/esm/types.native.js.map +0 -1
- package/dist/jsx/Toast.mjs +0 -131
- package/dist/jsx/Toast.mjs.map +0 -1
- package/dist/jsx/Toast.native.js +0 -174
- package/dist/jsx/Toast.native.js.map +0 -1
- package/dist/jsx/ToastAnnounce.mjs +0 -62
- package/dist/jsx/ToastAnnounce.mjs.map +0 -1
- package/dist/jsx/ToastAnnounce.native.js +0 -105
- package/dist/jsx/ToastAnnounce.native.js.map +0 -1
- package/dist/jsx/ToastImperative.mjs +0 -63
- package/dist/jsx/ToastImperative.mjs.map +0 -1
- package/dist/jsx/ToastImperative.native.js +0 -122
- package/dist/jsx/ToastImperative.native.js.map +0 -1
- package/dist/jsx/ToastImpl.mjs +0 -256
- package/dist/jsx/ToastImpl.mjs.map +0 -1
- package/dist/jsx/ToastImpl.native.js +0 -326
- package/dist/jsx/ToastImpl.native.js.map +0 -1
- package/dist/jsx/ToastPortal.mjs +0 -21
- package/dist/jsx/ToastPortal.mjs.map +0 -1
- package/dist/jsx/ToastPortal.native.js +0 -47
- package/dist/jsx/ToastPortal.native.js.map +0 -1
- package/dist/jsx/ToastProvider.mjs +0 -108
- package/dist/jsx/ToastProvider.mjs.map +0 -1
- package/dist/jsx/ToastProvider.native.js +0 -159
- package/dist/jsx/ToastProvider.native.js.map +0 -1
- package/dist/jsx/ToastViewport.mjs +0 -241
- package/dist/jsx/ToastViewport.mjs.map +0 -1
- package/dist/jsx/ToastViewport.native.js +0 -315
- package/dist/jsx/ToastViewport.native.js.map +0 -1
- package/dist/jsx/constants.mjs +0 -4
- package/dist/jsx/constants.mjs.map +0 -1
- package/dist/jsx/constants.native.js +0 -31
- package/dist/jsx/constants.native.js.map +0 -1
- package/dist/jsx/createNativeToast.mjs +0 -27
- package/dist/jsx/createNativeToast.mjs.map +0 -1
- package/dist/jsx/createNativeToast.native.js +0 -48
- package/dist/jsx/createNativeToast.native.js.map +0 -1
- package/dist/jsx/index.js +0 -2
- package/dist/jsx/index.js.map +0 -1
- package/dist/jsx/index.mjs +0 -2
- package/dist/jsx/index.mjs.map +0 -1
- package/dist/jsx/index.native.js +0 -21
- package/dist/jsx/index.native.js.map +0 -1
- package/dist/jsx/types.mjs +0 -2
- package/dist/jsx/types.mjs.map +0 -1
- package/dist/jsx/types.native.js +0 -19
- package/dist/jsx/types.native.js.map +0 -1
package/src/ToastImpl.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useIsPresent } from '@hanzogui/animate-presence'
|
|
2
2
|
import { useComposedRefs } from '@hanzogui/compose-refs'
|
|
3
3
|
import { isWeb } from '@hanzogui/constants'
|
|
4
|
-
import type { GetProps,
|
|
4
|
+
import type { GetProps, HanzoguiElement } from '@hanzogui/core'
|
|
5
5
|
import {
|
|
6
6
|
View,
|
|
7
7
|
Theme,
|
|
@@ -21,8 +21,17 @@ import type {
|
|
|
21
21
|
Animated,
|
|
22
22
|
GestureResponderEvent,
|
|
23
23
|
PanResponderGestureState,
|
|
24
|
+
PanResponder as PanResponderType,
|
|
24
25
|
} from 'react-native'
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
// Lazy load PanResponder only on native to avoid SSR issues
|
|
28
|
+
const getPanResponder = (): typeof PanResponderType | null => {
|
|
29
|
+
if (process.env.TAMAGUI_TARGET === 'native') {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
return require('react-native').PanResponder
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
26
35
|
import { TOAST_CONTEXT, TOAST_NAME } from './constants'
|
|
27
36
|
import { ToastAnnounce } from './ToastAnnounce'
|
|
28
37
|
import type { ScopedProps, SwipeDirection } from './ToastProvider'
|
|
@@ -53,7 +62,7 @@ const ToastImplFrame = styled(YStack, {
|
|
|
53
62
|
} as const,
|
|
54
63
|
|
|
55
64
|
defaultVariants: {
|
|
56
|
-
unstyled: process.env.
|
|
65
|
+
unstyled: process.env.HANZOGUI_HEADLESS === '1',
|
|
57
66
|
},
|
|
58
67
|
})
|
|
59
68
|
|
|
@@ -152,7 +161,7 @@ type ToastImplProps = ScopedProps<
|
|
|
152
161
|
ToastImplPrivateProps & ToastImplFrameProps & ToastExtraProps
|
|
153
162
|
>
|
|
154
163
|
|
|
155
|
-
const ToastImpl = React.forwardRef<
|
|
164
|
+
const ToastImpl = React.forwardRef<HanzoguiElement, ToastImplProps>(
|
|
156
165
|
(props, forwardedRef) => {
|
|
157
166
|
const {
|
|
158
167
|
scope,
|
|
@@ -172,7 +181,7 @@ const ToastImpl = React.forwardRef<GuiElement, ToastImplProps>(
|
|
|
172
181
|
} = props
|
|
173
182
|
const isPresent = useIsPresent()
|
|
174
183
|
const context = useToastProviderContext(scope)
|
|
175
|
-
const [node, setNode] = React.useState<
|
|
184
|
+
const [node, setNode] = React.useState<HanzoguiElement | null>(null)
|
|
176
185
|
const composedRefs = useComposedRefs(forwardedRef, setNode)
|
|
177
186
|
const duration = durationProp || context.duration
|
|
178
187
|
const closeTimerStartTimeRef = React.useRef(0)
|
|
@@ -261,7 +270,7 @@ const ToastImpl = React.forwardRef<GuiElement, ToastImplProps>(
|
|
|
261
270
|
|
|
262
271
|
const { animationDriver } = useConfiguration()
|
|
263
272
|
if (!animationDriver) {
|
|
264
|
-
throw new Error('Must set animations in
|
|
273
|
+
throw new Error('Must set animations in hanzogui.config.ts')
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
const { useAnimatedNumber, useAnimatedNumberStyle } = animationDriver
|
|
@@ -281,6 +290,8 @@ const ToastImpl = React.forwardRef<GuiElement, ToastImplProps>(
|
|
|
281
290
|
})
|
|
282
291
|
|
|
283
292
|
const panResponder = React.useMemo(() => {
|
|
293
|
+
const PanResponder = getPanResponder()
|
|
294
|
+
if (!PanResponder) return null
|
|
284
295
|
return PanResponder.create({
|
|
285
296
|
onMoveShouldSetPanResponder: (e, gesture) => {
|
|
286
297
|
const shouldMove = shouldGrantGestureMove(context.swipeDirection, gesture)
|
|
@@ -330,7 +341,8 @@ const ToastImpl = React.forwardRef<GuiElement, ToastImplProps>(
|
|
|
330
341
|
{announceTextContent && (
|
|
331
342
|
<ToastAnnounce
|
|
332
343
|
scope={scope}
|
|
333
|
-
//
|
|
344
|
+
// Toasts are always role=status to avoid stuttering issues with role=alert in SRs.
|
|
345
|
+
// biome-ignore lint/a11y/useSemanticElements: <explanation>
|
|
334
346
|
role="status"
|
|
335
347
|
aria-live={type === 'foreground' ? 'assertive' : 'polite'}
|
|
336
348
|
aria-atomic
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared visual styled components for toast items.
|
|
3
|
+
* Shared across web and native — imported by ToastComposable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { styled } from '@hanzogui/core'
|
|
7
|
+
import { XStack, YStack } from '@hanzogui/stacks'
|
|
8
|
+
import { SizableText } from '@hanzogui/text'
|
|
9
|
+
|
|
10
|
+
/* -------------------------------------------------------------------------------------------------
|
|
11
|
+
* ToastPositionWrapper - handles absolute positioning and stacking animations
|
|
12
|
+
* On web: uses Hanzogui transition/enterStyle/exitStyle
|
|
13
|
+
* On native: replaced by Animated.View with useAnimatedStyle
|
|
14
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
15
|
+
|
|
16
|
+
export const ToastPositionWrapper = styled(YStack, {
|
|
17
|
+
name: 'ToastPositionWrapper',
|
|
18
|
+
pointerEvents: 'auto',
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
left: 0,
|
|
21
|
+
right: 0,
|
|
22
|
+
opacity: 1,
|
|
23
|
+
scale: 1,
|
|
24
|
+
y: 0,
|
|
25
|
+
x: 0,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/* -------------------------------------------------------------------------------------------------
|
|
29
|
+
* ToastItemFrame - visual styling for the toast
|
|
30
|
+
* Shared across web and native — the visual appearance of the toast card.
|
|
31
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
32
|
+
|
|
33
|
+
export const ToastItemFrame = styled(YStack, {
|
|
34
|
+
name: 'ToastItem',
|
|
35
|
+
userSelect: 'none',
|
|
36
|
+
cursor: 'default',
|
|
37
|
+
focusable: true,
|
|
38
|
+
|
|
39
|
+
variants: {
|
|
40
|
+
unstyled: {
|
|
41
|
+
false: {
|
|
42
|
+
backgroundColor: '$background',
|
|
43
|
+
borderRadius: '$6',
|
|
44
|
+
paddingHorizontal: '$4',
|
|
45
|
+
paddingVertical: '$3',
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: '$borderColor',
|
|
48
|
+
shadowColor: 'rgba(0, 0, 0, 0.15)',
|
|
49
|
+
shadowOffset: { width: 0, height: 4 },
|
|
50
|
+
shadowRadius: 12,
|
|
51
|
+
focusVisibleStyle: {
|
|
52
|
+
outlineWidth: 2,
|
|
53
|
+
outlineColor: '$color8',
|
|
54
|
+
outlineStyle: 'solid',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
} as const,
|
|
59
|
+
|
|
60
|
+
defaultVariants: {
|
|
61
|
+
unstyled: process.env.HANZOGUI_HEADLESS === '1',
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/* -------------------------------------------------------------------------------------------------
|
|
66
|
+
* ToastCloseFrame
|
|
67
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
68
|
+
|
|
69
|
+
export const ToastCloseFrame = styled(XStack, {
|
|
70
|
+
name: 'ToastClose',
|
|
71
|
+
render: 'button',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
cursor: 'pointer',
|
|
75
|
+
|
|
76
|
+
variants: {
|
|
77
|
+
unstyled: {
|
|
78
|
+
false: {
|
|
79
|
+
width: 18,
|
|
80
|
+
height: 18,
|
|
81
|
+
borderRadius: '$10',
|
|
82
|
+
backgroundColor: '$background',
|
|
83
|
+
borderWidth: 1,
|
|
84
|
+
borderColor: '$borderColor',
|
|
85
|
+
shadowColor: 'rgba(0, 0, 0, 0.08)',
|
|
86
|
+
shadowOffset: { width: 0, height: 1 },
|
|
87
|
+
shadowRadius: 3,
|
|
88
|
+
hoverStyle: { backgroundColor: '$color3' },
|
|
89
|
+
pressStyle: { backgroundColor: '$color4' },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
} as const,
|
|
93
|
+
|
|
94
|
+
defaultVariants: {
|
|
95
|
+
unstyled: process.env.HANZOGUI_HEADLESS === '1',
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/* -------------------------------------------------------------------------------------------------
|
|
100
|
+
* ToastActionFrame - for action/cancel buttons with text
|
|
101
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
102
|
+
|
|
103
|
+
export const ToastActionFrame = styled(XStack, {
|
|
104
|
+
name: 'ToastAction',
|
|
105
|
+
render: 'button',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
cursor: 'pointer',
|
|
109
|
+
|
|
110
|
+
variants: {
|
|
111
|
+
unstyled: {
|
|
112
|
+
false: {
|
|
113
|
+
borderRadius: '$2',
|
|
114
|
+
paddingHorizontal: '$2',
|
|
115
|
+
height: 24,
|
|
116
|
+
backgroundColor: '$color5',
|
|
117
|
+
hoverStyle: { backgroundColor: '$color6' },
|
|
118
|
+
pressStyle: { backgroundColor: '$color7' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
} as const,
|
|
122
|
+
|
|
123
|
+
defaultVariants: {
|
|
124
|
+
unstyled: process.env.HANZOGUI_HEADLESS === '1',
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
/* -------------------------------------------------------------------------------------------------
|
|
129
|
+
* DefaultCloseIcon
|
|
130
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
131
|
+
|
|
132
|
+
export const DefaultCloseIcon = () => (
|
|
133
|
+
<SizableText size="$1" color="$color11">
|
|
134
|
+
✕
|
|
135
|
+
</SizableText>
|
|
136
|
+
)
|
package/src/ToastPortal.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { isWeb } from '@hanzogui/constants'
|
|
1
2
|
import { Portal } from '@hanzogui/portal'
|
|
2
3
|
import type { ReactNode } from 'react'
|
|
3
|
-
import { Platform } from 'react-native'
|
|
4
4
|
import { ReprogapateToastProvider, type ToastProviderContextValue } from './ToastProvider'
|
|
5
5
|
|
|
6
6
|
export function ToastPortal(props: {
|
|
@@ -10,7 +10,7 @@ export function ToastPortal(props: {
|
|
|
10
10
|
}) {
|
|
11
11
|
const { context, children, zIndex } = props
|
|
12
12
|
let content = children
|
|
13
|
-
if (
|
|
13
|
+
if (!isWeb) {
|
|
14
14
|
content = (
|
|
15
15
|
<ReprogapateToastProvider context={context}>{children}</ReprogapateToastProvider>
|
|
16
16
|
)
|
package/src/ToastProvider.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCollection } from '@hanzogui/collection'
|
|
2
|
-
import type { NativeValue,
|
|
2
|
+
import type { NativeValue, HanzoguiElement } from '@hanzogui/core'
|
|
3
3
|
import { createStyledContext } from '@hanzogui/core'
|
|
4
4
|
import { startTransition } from '@hanzogui/start-transition'
|
|
5
5
|
import * as React from 'react'
|
|
@@ -15,9 +15,16 @@ import type { BurntToastOptions } from './types'
|
|
|
15
15
|
|
|
16
16
|
const PROVIDER_NAME = 'ToastProvider'
|
|
17
17
|
|
|
18
|
-
const [Collection, useCollection] = createCollection<
|
|
18
|
+
const [Collection, useCollection] = createCollection<HanzoguiElement>('Toast')
|
|
19
19
|
|
|
20
|
-
export type SwipeDirection =
|
|
20
|
+
export type SwipeDirection =
|
|
21
|
+
| 'vertical'
|
|
22
|
+
| 'up'
|
|
23
|
+
| 'down'
|
|
24
|
+
| 'horizontal'
|
|
25
|
+
| 'left'
|
|
26
|
+
| 'right'
|
|
27
|
+
| 'auto'
|
|
21
28
|
|
|
22
29
|
export type ToastProviderContextValue = {
|
|
23
30
|
id: string
|
|
@@ -27,8 +34,8 @@ export type ToastProviderContextValue = {
|
|
|
27
34
|
swipeDirection: SwipeDirection
|
|
28
35
|
swipeThreshold: number
|
|
29
36
|
toastCount: number
|
|
30
|
-
viewports: Record<string,
|
|
31
|
-
onViewportChange(name: string, viewport:
|
|
37
|
+
viewports: Record<string, HanzoguiElement | null>
|
|
38
|
+
onViewportChange(name: string, viewport: HanzoguiElement): void
|
|
32
39
|
onToastAdd(): void
|
|
33
40
|
onToastRemove(): void
|
|
34
41
|
isFocusedToastEscapeKeyDownRef: React.MutableRefObject<boolean>
|
|
@@ -114,7 +121,7 @@ const ToastProvider: React.FC<ToastProviderProps> = (
|
|
|
114
121
|
const isClosePausedRef = React.useRef(false)
|
|
115
122
|
|
|
116
123
|
const handleViewportChange = React.useCallback(
|
|
117
|
-
(name: string, viewport:
|
|
124
|
+
(name: string, viewport: HanzoguiElement | null) => {
|
|
118
125
|
startTransition(() => {
|
|
119
126
|
setViewports((prev) => ({ ...prev, [name]: viewport }))
|
|
120
127
|
})
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import type { CreateNativeToastOptions, NativeToastRef } from './types'
|
|
3
|
+
|
|
4
|
+
// counter for generating unique toast ids
|
|
5
|
+
let toastsCounter = 1
|
|
6
|
+
|
|
7
|
+
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading'
|
|
8
|
+
|
|
9
|
+
export interface ToastT {
|
|
10
|
+
id: string | number
|
|
11
|
+
title: React.ReactNode | (() => React.ReactNode)
|
|
12
|
+
description?: React.ReactNode | (() => React.ReactNode)
|
|
13
|
+
type?: ToastType
|
|
14
|
+
icon?: React.ReactNode
|
|
15
|
+
jsx?: React.ReactElement
|
|
16
|
+
dismissible?: boolean
|
|
17
|
+
duration?: number
|
|
18
|
+
promise?: PromiseT
|
|
19
|
+
action?: ToastAction
|
|
20
|
+
cancel?: ToastAction
|
|
21
|
+
closeButton?: boolean
|
|
22
|
+
onDismiss?: (toast: ToastT) => void
|
|
23
|
+
onAutoClose?: (toast: ToastT) => void
|
|
24
|
+
// internal
|
|
25
|
+
delete?: boolean
|
|
26
|
+
// style overrides
|
|
27
|
+
style?: React.CSSProperties
|
|
28
|
+
className?: string
|
|
29
|
+
// native options
|
|
30
|
+
burntOptions?: CreateNativeToastOptions['burntOptions']
|
|
31
|
+
notificationOptions?: CreateNativeToastOptions['notificationOptions']
|
|
32
|
+
/** Custom user data */
|
|
33
|
+
data?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ToastAction {
|
|
37
|
+
label: string
|
|
38
|
+
onClick?: (event: React.MouseEvent) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ToastToDismiss {
|
|
42
|
+
id: string | number
|
|
43
|
+
dismiss: true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ExternalToast = Omit<
|
|
47
|
+
ToastT,
|
|
48
|
+
'id' | 'title' | 'type' | 'delete' | 'promise'
|
|
49
|
+
> & {
|
|
50
|
+
id?: string | number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type PromiseT<Data = any> = Promise<Data> | (() => Promise<Data>)
|
|
54
|
+
|
|
55
|
+
export interface PromiseData<Data = any> {
|
|
56
|
+
loading?: React.ReactNode | (() => React.ReactNode)
|
|
57
|
+
success?: React.ReactNode | ((data: Data) => React.ReactNode)
|
|
58
|
+
error?: React.ReactNode | ((error: any) => React.ReactNode)
|
|
59
|
+
description?: React.ReactNode | ((data: any) => React.ReactNode)
|
|
60
|
+
finally?: () => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type TitleT = React.ReactNode | (() => React.ReactNode)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Observer class that manages toast state globally.
|
|
67
|
+
* Follows the pattern from Sonner for a clean, decoupled architecture.
|
|
68
|
+
*/
|
|
69
|
+
class Observer {
|
|
70
|
+
subscribers: Array<(toast: ToastT | ToastToDismiss) => void> = []
|
|
71
|
+
toasts: ToastT[] = []
|
|
72
|
+
dismissedToasts: Set<string | number> = new Set()
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Subscribe to toast state changes.
|
|
76
|
+
* Returns an unsubscribe function.
|
|
77
|
+
*/
|
|
78
|
+
subscribe = (subscriber: (toast: ToastT | ToastToDismiss) => void) => {
|
|
79
|
+
this.subscribers.push(subscriber)
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
const index = this.subscribers.indexOf(subscriber)
|
|
83
|
+
if (index > -1) {
|
|
84
|
+
this.subscribers.splice(index, 1)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Publish a toast to all subscribers
|
|
91
|
+
*/
|
|
92
|
+
publish = (data: ToastT) => {
|
|
93
|
+
this.subscribers.forEach((subscriber) => subscriber(data))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Add a new toast to the internal array and publish to subscribers
|
|
98
|
+
*/
|
|
99
|
+
addToast = (data: ToastT) => {
|
|
100
|
+
this.publish(data)
|
|
101
|
+
this.toasts = [...this.toasts, data]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create or update a toast
|
|
106
|
+
*/
|
|
107
|
+
create = (
|
|
108
|
+
data: ExternalToast & {
|
|
109
|
+
title?: TitleT
|
|
110
|
+
type?: ToastType
|
|
111
|
+
promise?: PromiseT
|
|
112
|
+
jsx?: React.ReactElement
|
|
113
|
+
}
|
|
114
|
+
) => {
|
|
115
|
+
const { title, ...rest } = data
|
|
116
|
+
const id =
|
|
117
|
+
typeof data?.id === 'number' || (typeof data?.id === 'string' && data.id.length > 0)
|
|
118
|
+
? data.id
|
|
119
|
+
: toastsCounter++
|
|
120
|
+
|
|
121
|
+
const alreadyExists = this.toasts.find((toast) => toast.id === id)
|
|
122
|
+
const dismissible = data.dismissible ?? true
|
|
123
|
+
|
|
124
|
+
// if this toast was previously dismissed, clear that
|
|
125
|
+
if (this.dismissedToasts.has(id)) {
|
|
126
|
+
this.dismissedToasts.delete(id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (alreadyExists) {
|
|
130
|
+
// update existing toast
|
|
131
|
+
this.toasts = this.toasts.map((toast) => {
|
|
132
|
+
if (toast.id === id) {
|
|
133
|
+
this.publish({ ...toast, ...data, id, title, dismissible })
|
|
134
|
+
return { ...toast, ...data, id, title, dismissible }
|
|
135
|
+
}
|
|
136
|
+
return toast
|
|
137
|
+
})
|
|
138
|
+
} else {
|
|
139
|
+
this.addToast({ title, ...rest, dismissible, id } as ToastT)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return id
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dismiss a toast by id, or all toasts if no id provided
|
|
147
|
+
*/
|
|
148
|
+
dismiss = (id?: string | number) => {
|
|
149
|
+
if (id !== undefined) {
|
|
150
|
+
this.dismissedToasts.add(id)
|
|
151
|
+
// use requestAnimationFrame to batch updates
|
|
152
|
+
requestAnimationFrame(() => {
|
|
153
|
+
this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true }))
|
|
154
|
+
})
|
|
155
|
+
} else {
|
|
156
|
+
// dismiss all
|
|
157
|
+
this.toasts.forEach((toast) => {
|
|
158
|
+
this.subscribers.forEach((subscriber) =>
|
|
159
|
+
subscriber({ id: toast.id, dismiss: true })
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return id
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Show a basic toast message
|
|
169
|
+
*/
|
|
170
|
+
message = (title: TitleT, data?: ExternalToast) => {
|
|
171
|
+
return this.create({ ...data, title, type: 'default' })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Show a success toast
|
|
176
|
+
*/
|
|
177
|
+
success = (title: TitleT, data?: ExternalToast) => {
|
|
178
|
+
return this.create({ ...data, title, type: 'success' })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Show an error toast
|
|
183
|
+
*/
|
|
184
|
+
error = (title: TitleT, data?: ExternalToast) => {
|
|
185
|
+
return this.create({ ...data, title, type: 'error' })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Show a warning toast
|
|
190
|
+
*/
|
|
191
|
+
warning = (title: TitleT, data?: ExternalToast) => {
|
|
192
|
+
return this.create({ ...data, title, type: 'warning' })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Show an info toast
|
|
197
|
+
*/
|
|
198
|
+
info = (title: TitleT, data?: ExternalToast) => {
|
|
199
|
+
return this.create({ ...data, title, type: 'info' })
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Show a loading toast
|
|
204
|
+
*/
|
|
205
|
+
loading = (title: TitleT, data?: ExternalToast) => {
|
|
206
|
+
return this.create({ ...data, title, type: 'loading' })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Show a toast for a promise, automatically transitioning through
|
|
211
|
+
* loading -> success/error states
|
|
212
|
+
*/
|
|
213
|
+
promise = <ToastData>(promise: PromiseT<ToastData>, data?: PromiseData<ToastData>) => {
|
|
214
|
+
if (!data) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let id: string | number | undefined = undefined
|
|
219
|
+
|
|
220
|
+
// show loading state if provided
|
|
221
|
+
if (data.loading !== undefined) {
|
|
222
|
+
id = this.create({
|
|
223
|
+
promise,
|
|
224
|
+
type: 'loading',
|
|
225
|
+
title: data.loading,
|
|
226
|
+
description:
|
|
227
|
+
typeof data.description !== 'function' ? data.description : undefined,
|
|
228
|
+
// loading toasts shouldn't auto-dismiss
|
|
229
|
+
duration: Number.POSITIVE_INFINITY,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const p = Promise.resolve(promise instanceof Function ? promise() : promise)
|
|
234
|
+
|
|
235
|
+
let shouldDismiss = id !== undefined
|
|
236
|
+
let result: ['resolve', ToastData] | ['reject', unknown]
|
|
237
|
+
|
|
238
|
+
const originalPromise = p
|
|
239
|
+
.then(async (response) => {
|
|
240
|
+
result = ['resolve', response]
|
|
241
|
+
|
|
242
|
+
// check if response is an HTTP error
|
|
243
|
+
if (isHttpResponse(response) && !response.ok) {
|
|
244
|
+
shouldDismiss = false
|
|
245
|
+
const errorMsg =
|
|
246
|
+
typeof data.error === 'function'
|
|
247
|
+
? await data.error(`HTTP error! status: ${response.status}`)
|
|
248
|
+
: data.error
|
|
249
|
+
const description =
|
|
250
|
+
typeof data.description === 'function'
|
|
251
|
+
? await data.description(`HTTP error! status: ${response.status}`)
|
|
252
|
+
: data.description
|
|
253
|
+
|
|
254
|
+
this.create({ id, type: 'error', title: errorMsg, description })
|
|
255
|
+
} else if (data.success !== undefined) {
|
|
256
|
+
shouldDismiss = false
|
|
257
|
+
const successMsg =
|
|
258
|
+
typeof data.success === 'function'
|
|
259
|
+
? await data.success(response)
|
|
260
|
+
: data.success
|
|
261
|
+
const description =
|
|
262
|
+
typeof data.description === 'function'
|
|
263
|
+
? await data.description(response)
|
|
264
|
+
: data.description
|
|
265
|
+
|
|
266
|
+
this.create({ id, type: 'success', title: successMsg, description })
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
.catch(async (error) => {
|
|
270
|
+
result = ['reject', error]
|
|
271
|
+
|
|
272
|
+
if (data.error !== undefined) {
|
|
273
|
+
shouldDismiss = false
|
|
274
|
+
const errorMsg =
|
|
275
|
+
typeof data.error === 'function' ? await data.error(error) : data.error
|
|
276
|
+
const description =
|
|
277
|
+
typeof data.description === 'function'
|
|
278
|
+
? await data.description(error)
|
|
279
|
+
: data.description
|
|
280
|
+
|
|
281
|
+
this.create({ id, type: 'error', title: errorMsg, description })
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
.finally(() => {
|
|
285
|
+
if (shouldDismiss) {
|
|
286
|
+
// toast is still in load state, dismiss it
|
|
287
|
+
this.dismiss(id)
|
|
288
|
+
id = undefined
|
|
289
|
+
}
|
|
290
|
+
data.finally?.()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// return a promise that can be unwrapped
|
|
294
|
+
const unwrap = () =>
|
|
295
|
+
new Promise<ToastData>((resolve, reject) =>
|
|
296
|
+
originalPromise
|
|
297
|
+
.then(() => (result[0] === 'reject' ? reject(result[1]) : resolve(result[1])))
|
|
298
|
+
.catch(reject)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if (typeof id !== 'string' && typeof id !== 'number') {
|
|
302
|
+
return { unwrap }
|
|
303
|
+
} else {
|
|
304
|
+
return Object.assign(id, { unwrap })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Show a custom JSX toast
|
|
310
|
+
*/
|
|
311
|
+
custom = (jsx: (id: string | number) => React.ReactElement, data?: ExternalToast) => {
|
|
312
|
+
const id = data?.id ?? toastsCounter++
|
|
313
|
+
this.create({ jsx: jsx(id), ...data, id })
|
|
314
|
+
return id
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get all active (non-dismissed) toasts
|
|
319
|
+
*/
|
|
320
|
+
getActiveToasts = () => {
|
|
321
|
+
return this.toasts.filter((toast) => !this.dismissedToasts.has(toast.id))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get full toast history
|
|
326
|
+
*/
|
|
327
|
+
getHistory = () => {
|
|
328
|
+
return this.toasts
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function isHttpResponse(data: any): data is Response {
|
|
333
|
+
return (
|
|
334
|
+
data &&
|
|
335
|
+
typeof data === 'object' &&
|
|
336
|
+
'ok' in data &&
|
|
337
|
+
typeof data.ok === 'boolean' &&
|
|
338
|
+
'status' in data &&
|
|
339
|
+
typeof data.status === 'number'
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// singleton instance
|
|
344
|
+
export const ToastState = new Observer()
|
|
345
|
+
|
|
346
|
+
// basic toast function
|
|
347
|
+
const toastFunction = (title: TitleT, data?: ExternalToast) => {
|
|
348
|
+
return ToastState.create({ ...data, title })
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// getters
|
|
352
|
+
const getHistory = () => ToastState.getHistory()
|
|
353
|
+
const getToasts = () => ToastState.getActiveToasts()
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Main toast API - call directly or use methods like toast.success()
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* // basic usage
|
|
360
|
+
* toast('Hello world')
|
|
361
|
+
*
|
|
362
|
+
* // with type
|
|
363
|
+
* toast.success('Saved!')
|
|
364
|
+
* toast.error('Something went wrong')
|
|
365
|
+
*
|
|
366
|
+
* // with options
|
|
367
|
+
* toast('Hello', { duration: 5000 })
|
|
368
|
+
*
|
|
369
|
+
* // promise toast
|
|
370
|
+
* toast.promise(fetch('/api'), {
|
|
371
|
+
* loading: 'Loading...',
|
|
372
|
+
* success: 'Done!',
|
|
373
|
+
* error: 'Failed'
|
|
374
|
+
* })
|
|
375
|
+
*
|
|
376
|
+
* // custom JSX
|
|
377
|
+
* toast.custom((id) => <MyToast id={id} />)
|
|
378
|
+
*
|
|
379
|
+
* // dismiss
|
|
380
|
+
* const id = toast('Hello')
|
|
381
|
+
* toast.dismiss(id)
|
|
382
|
+
* toast.dismiss() // dismiss all
|
|
383
|
+
*/
|
|
384
|
+
export const toast = Object.assign(toastFunction, {
|
|
385
|
+
success: ToastState.success,
|
|
386
|
+
error: ToastState.error,
|
|
387
|
+
warning: ToastState.warning,
|
|
388
|
+
info: ToastState.info,
|
|
389
|
+
loading: ToastState.loading,
|
|
390
|
+
promise: ToastState.promise,
|
|
391
|
+
custom: ToastState.custom,
|
|
392
|
+
dismiss: ToastState.dismiss,
|
|
393
|
+
message: ToastState.message,
|
|
394
|
+
getHistory,
|
|
395
|
+
getToasts,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
export type { NativeToastRef }
|