@sudobility/components-rn 1.0.20 → 1.0.21
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/dist/index.cjs.js +2898 -77
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +2900 -79
- package/dist/index.esm.js.map +1 -1
- package/dist/ui/Banner/Banner.d.ts +97 -0
- package/dist/ui/Banner/Banner.d.ts.map +1 -0
- package/dist/ui/Banner/index.d.ts +2 -0
- package/dist/ui/Banner/index.d.ts.map +1 -0
- package/dist/ui/Button/Button.d.ts +2 -2
- package/package.json +3 -1
- package/src/index.ts +1 -0
- package/src/ui/Banner/Banner.tsx +315 -0
- package/src/ui/Banner/index.ts +7 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { InfoType } from '@sudobility/types';
|
|
3
|
+
export interface BannerProps {
|
|
4
|
+
/** Whether the banner is visible */
|
|
5
|
+
isVisible: boolean;
|
|
6
|
+
/** Callback when banner is dismissed */
|
|
7
|
+
onDismiss: () => void;
|
|
8
|
+
/** Banner title */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Banner description (optional) */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Visual variant */
|
|
13
|
+
variant?: InfoType;
|
|
14
|
+
/** Auto-dismiss duration in milliseconds (0 to disable, default: 5000) */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Custom icon (overrides variant icon) */
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
/** Additional NativeWind classes */
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Accessible label for close button */
|
|
21
|
+
closeAccessibilityLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Banner Component
|
|
25
|
+
*
|
|
26
|
+
* A temporary notification banner that slides down from the top of the screen.
|
|
27
|
+
* React Native port of the web Banner from @sudobility/components.
|
|
28
|
+
*
|
|
29
|
+
* Features:
|
|
30
|
+
* - Slides down from top with spring animation
|
|
31
|
+
* - Auto-dismisses after configurable duration (default 5 seconds)
|
|
32
|
+
* - Manual dismiss via close button
|
|
33
|
+
* - Multiple variants: info, success, warning, error
|
|
34
|
+
* - Dark/light theme support via NativeWind
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const [showBanner, setShowBanner] = useState(false);
|
|
39
|
+
*
|
|
40
|
+
* <Banner
|
|
41
|
+
* isVisible={showBanner}
|
|
42
|
+
* onDismiss={() => setShowBanner(false)}
|
|
43
|
+
* variant={InfoType.SUCCESS}
|
|
44
|
+
* title="Changes saved"
|
|
45
|
+
* description="Your settings have been updated successfully."
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const Banner: React.FC<BannerProps>;
|
|
50
|
+
/**
|
|
51
|
+
* Hook for managing banner state
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* const { isVisible, bannerProps, showBanner, hideBanner } = useBanner();
|
|
56
|
+
*
|
|
57
|
+
* showBanner({
|
|
58
|
+
* title: 'Success!',
|
|
59
|
+
* description: 'Your changes have been saved.',
|
|
60
|
+
* variant: InfoType.SUCCESS,
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* {bannerProps && (
|
|
64
|
+
* <Banner
|
|
65
|
+
* isVisible={isVisible}
|
|
66
|
+
* onDismiss={hideBanner}
|
|
67
|
+
* {...bannerProps}
|
|
68
|
+
* />
|
|
69
|
+
* )}
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export interface UseBannerOptions {
|
|
73
|
+
/** Default duration for auto-dismiss (default: 5000) */
|
|
74
|
+
defaultDuration?: number;
|
|
75
|
+
}
|
|
76
|
+
export interface UseBannerReturn {
|
|
77
|
+
/** Whether the banner is currently visible */
|
|
78
|
+
isVisible: boolean;
|
|
79
|
+
/** Current banner props */
|
|
80
|
+
bannerProps: {
|
|
81
|
+
title: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
variant: InfoType;
|
|
84
|
+
duration: number;
|
|
85
|
+
} | null;
|
|
86
|
+
/** Show a banner */
|
|
87
|
+
showBanner: (options: {
|
|
88
|
+
title: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
variant?: InfoType;
|
|
91
|
+
duration?: number;
|
|
92
|
+
}) => void;
|
|
93
|
+
/** Hide the current banner */
|
|
94
|
+
hideBanner: () => void;
|
|
95
|
+
}
|
|
96
|
+
export declare const useBanner: (options?: UseBannerOptions) => UseBannerReturn;
|
|
97
|
+
//# sourceMappingURL=Banner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Banner.d.ts","sourceRoot":"","sources":["../../../src/ui/Banner/Banner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAExE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AA6CD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAuIxC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,WAAW,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,QAAQ,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,IAAI,CAAC;IACT,oBAAoB;IACpB,UAAU,EAAE,CAAC,OAAO,EAAE;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,QAAQ,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,KAAK,IAAI,CAAC;IACX,8BAA8B;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,eAAO,MAAM,SAAS,GAAI,UAAS,gBAAqB,KAAG,eAkC1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/Banner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,SAAS,EACT,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC"}
|
|
@@ -3,8 +3,8 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
3
3
|
import { ButtonBaseProps } from './Button.shared';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "primary" | "secondary" | "outline" | "ghost" | "destructive" | "destructive-outline" | "success" | "
|
|
7
|
-
size?: "
|
|
6
|
+
variant?: "link" | "default" | "primary" | "secondary" | "outline" | "ghost" | "destructive" | "destructive-outline" | "success" | "gradient" | "gradient-secondary" | "gradient-success" | "wallet" | "connect" | "disconnect" | null | undefined;
|
|
7
|
+
size?: "icon" | "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
9
9
|
export interface ButtonProps extends ButtonBaseProps, Omit<PressableProps, 'children' | 'disabled'>, Omit<VariantProps<typeof buttonVariants>, 'variant' | 'size'> {
|
|
10
10
|
/** Callback when button is pressed */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/components-rn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "React Native UI components and design system - Ported from @sudobility/components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"author": "Sudobility",
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@sudobility/design": "^1.1.19",
|
|
52
|
+
"@sudobility/types": "^1.9.53",
|
|
52
53
|
"nativewind": ">=4.0.0",
|
|
53
54
|
"react": "^18.0.0 || ^19.0.0",
|
|
54
55
|
"react-native": ">=0.72.0",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@eslint/js": "^9.15.0",
|
|
64
65
|
"@sudobility/design": "^1.1.19",
|
|
66
|
+
"@sudobility/types": "^1.9.53",
|
|
65
67
|
"@testing-library/jest-native": "^5.4.3",
|
|
66
68
|
"@testing-library/react-native": "^13.3.3",
|
|
67
69
|
"@types/jest": "^29.5.0",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { View, Text, Pressable, Animated } from 'react-native';
|
|
3
|
+
import { InfoType } from '@sudobility/types';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
export interface BannerProps {
|
|
7
|
+
/** Whether the banner is visible */
|
|
8
|
+
isVisible: boolean;
|
|
9
|
+
/** Callback when banner is dismissed */
|
|
10
|
+
onDismiss: () => void;
|
|
11
|
+
/** Banner title */
|
|
12
|
+
title: string;
|
|
13
|
+
/** Banner description (optional) */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Visual variant */
|
|
16
|
+
variant?: InfoType;
|
|
17
|
+
/** Auto-dismiss duration in milliseconds (0 to disable, default: 5000) */
|
|
18
|
+
duration?: number;
|
|
19
|
+
/** Custom icon (overrides variant icon) */
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
/** Additional NativeWind classes */
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Accessible label for close button */
|
|
24
|
+
closeAccessibilityLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const variantConfig: Record<
|
|
28
|
+
InfoType,
|
|
29
|
+
{
|
|
30
|
+
icon: string;
|
|
31
|
+
container: string;
|
|
32
|
+
iconColor: string;
|
|
33
|
+
titleColor: string;
|
|
34
|
+
descriptionColor: string;
|
|
35
|
+
}
|
|
36
|
+
> = {
|
|
37
|
+
[InfoType.INFO]: {
|
|
38
|
+
icon: '\u2139',
|
|
39
|
+
container:
|
|
40
|
+
'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800',
|
|
41
|
+
iconColor: 'text-blue-600 dark:text-blue-400',
|
|
42
|
+
titleColor: 'text-blue-900 dark:text-blue-100',
|
|
43
|
+
descriptionColor: 'text-blue-700 dark:text-blue-300',
|
|
44
|
+
},
|
|
45
|
+
[InfoType.SUCCESS]: {
|
|
46
|
+
icon: '\u2713',
|
|
47
|
+
container:
|
|
48
|
+
'bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800',
|
|
49
|
+
iconColor: 'text-green-600 dark:text-green-400',
|
|
50
|
+
titleColor: 'text-green-900 dark:text-green-100',
|
|
51
|
+
descriptionColor: 'text-green-700 dark:text-green-300',
|
|
52
|
+
},
|
|
53
|
+
[InfoType.WARNING]: {
|
|
54
|
+
icon: '\u26A0',
|
|
55
|
+
container:
|
|
56
|
+
'bg-yellow-50 dark:bg-amber-950 border-yellow-200 dark:border-amber-800',
|
|
57
|
+
iconColor: 'text-yellow-600 dark:text-amber-400',
|
|
58
|
+
titleColor: 'text-yellow-900 dark:text-amber-100',
|
|
59
|
+
descriptionColor: 'text-yellow-700 dark:text-amber-300',
|
|
60
|
+
},
|
|
61
|
+
[InfoType.ERROR]: {
|
|
62
|
+
icon: '\u2717',
|
|
63
|
+
container: 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800',
|
|
64
|
+
iconColor: 'text-red-600 dark:text-red-400',
|
|
65
|
+
titleColor: 'text-red-900 dark:text-red-100',
|
|
66
|
+
descriptionColor: 'text-red-700 dark:text-red-300',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Banner Component
|
|
72
|
+
*
|
|
73
|
+
* A temporary notification banner that slides down from the top of the screen.
|
|
74
|
+
* React Native port of the web Banner from @sudobility/components.
|
|
75
|
+
*
|
|
76
|
+
* Features:
|
|
77
|
+
* - Slides down from top with spring animation
|
|
78
|
+
* - Auto-dismisses after configurable duration (default 5 seconds)
|
|
79
|
+
* - Manual dismiss via close button
|
|
80
|
+
* - Multiple variants: info, success, warning, error
|
|
81
|
+
* - Dark/light theme support via NativeWind
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* const [showBanner, setShowBanner] = useState(false);
|
|
86
|
+
*
|
|
87
|
+
* <Banner
|
|
88
|
+
* isVisible={showBanner}
|
|
89
|
+
* onDismiss={() => setShowBanner(false)}
|
|
90
|
+
* variant={InfoType.SUCCESS}
|
|
91
|
+
* title="Changes saved"
|
|
92
|
+
* description="Your settings have been updated successfully."
|
|
93
|
+
* />
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export const Banner: React.FC<BannerProps> = ({
|
|
97
|
+
isVisible,
|
|
98
|
+
onDismiss,
|
|
99
|
+
title,
|
|
100
|
+
description,
|
|
101
|
+
variant = InfoType.INFO,
|
|
102
|
+
duration = 5000,
|
|
103
|
+
icon,
|
|
104
|
+
className,
|
|
105
|
+
closeAccessibilityLabel = 'Dismiss banner',
|
|
106
|
+
}) => {
|
|
107
|
+
const [mounted, setMounted] = useState(false);
|
|
108
|
+
const slideAnim = useRef(new Animated.Value(-100)).current;
|
|
109
|
+
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
110
|
+
const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
111
|
+
|
|
112
|
+
const config = variantConfig[variant];
|
|
113
|
+
|
|
114
|
+
const clearDismissTimeout = useCallback(() => {
|
|
115
|
+
if (dismissTimeoutRef.current) {
|
|
116
|
+
clearTimeout(dismissTimeoutRef.current);
|
|
117
|
+
dismissTimeoutRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const handleDismiss = useCallback(() => {
|
|
122
|
+
clearDismissTimeout();
|
|
123
|
+
Animated.parallel([
|
|
124
|
+
Animated.timing(slideAnim, {
|
|
125
|
+
toValue: -100,
|
|
126
|
+
duration: 300,
|
|
127
|
+
useNativeDriver: true,
|
|
128
|
+
}),
|
|
129
|
+
Animated.timing(opacityAnim, {
|
|
130
|
+
toValue: 0,
|
|
131
|
+
duration: 300,
|
|
132
|
+
useNativeDriver: true,
|
|
133
|
+
}),
|
|
134
|
+
]).start(() => {
|
|
135
|
+
setMounted(false);
|
|
136
|
+
onDismiss();
|
|
137
|
+
});
|
|
138
|
+
}, [clearDismissTimeout, slideAnim, opacityAnim, onDismiss]);
|
|
139
|
+
|
|
140
|
+
// Handle visibility changes
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (isVisible) {
|
|
143
|
+
setMounted(true);
|
|
144
|
+
slideAnim.setValue(-100);
|
|
145
|
+
opacityAnim.setValue(0);
|
|
146
|
+
|
|
147
|
+
Animated.parallel([
|
|
148
|
+
Animated.spring(slideAnim, {
|
|
149
|
+
toValue: 0,
|
|
150
|
+
useNativeDriver: true,
|
|
151
|
+
friction: 8,
|
|
152
|
+
}),
|
|
153
|
+
Animated.timing(opacityAnim, {
|
|
154
|
+
toValue: 1,
|
|
155
|
+
duration: 300,
|
|
156
|
+
useNativeDriver: true,
|
|
157
|
+
}),
|
|
158
|
+
]).start(() => {
|
|
159
|
+
// Start auto-dismiss timer after animation completes
|
|
160
|
+
if (duration > 0) {
|
|
161
|
+
dismissTimeoutRef.current = setTimeout(() => {
|
|
162
|
+
handleDismiss();
|
|
163
|
+
}, duration);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else if (mounted) {
|
|
167
|
+
handleDismiss();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return () => clearDismissTimeout();
|
|
171
|
+
}, [isVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
172
|
+
|
|
173
|
+
// Cleanup on unmount
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
return () => clearDismissTimeout();
|
|
176
|
+
}, [clearDismissTimeout]);
|
|
177
|
+
|
|
178
|
+
if (!mounted) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Animated.View
|
|
184
|
+
style={{
|
|
185
|
+
transform: [{ translateY: slideAnim }],
|
|
186
|
+
opacity: opacityAnim,
|
|
187
|
+
}}
|
|
188
|
+
className={cn('absolute top-0 left-0 right-0 z-50', className)}
|
|
189
|
+
accessibilityRole='alert'
|
|
190
|
+
accessibilityLiveRegion='polite'
|
|
191
|
+
>
|
|
192
|
+
<View
|
|
193
|
+
className={cn(
|
|
194
|
+
'flex-row items-start gap-3 p-4 border-b',
|
|
195
|
+
config.container
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{/* Icon */}
|
|
199
|
+
<View className='flex-shrink-0 mt-0.5'>
|
|
200
|
+
{icon || (
|
|
201
|
+
<Text className={cn('text-lg', config.iconColor)}>
|
|
202
|
+
{config.icon}
|
|
203
|
+
</Text>
|
|
204
|
+
)}
|
|
205
|
+
</View>
|
|
206
|
+
|
|
207
|
+
{/* Content */}
|
|
208
|
+
<View className='flex-1 min-w-0'>
|
|
209
|
+
<Text className={cn('font-semibold', config.titleColor)}>
|
|
210
|
+
{title}
|
|
211
|
+
</Text>
|
|
212
|
+
{description ? (
|
|
213
|
+
<Text className={cn('text-sm mt-1', config.descriptionColor)}>
|
|
214
|
+
{description}
|
|
215
|
+
</Text>
|
|
216
|
+
) : null}
|
|
217
|
+
</View>
|
|
218
|
+
|
|
219
|
+
{/* Close button */}
|
|
220
|
+
<Pressable
|
|
221
|
+
onPress={handleDismiss}
|
|
222
|
+
className='flex-shrink-0 p-1 rounded-md'
|
|
223
|
+
accessibilityRole='button'
|
|
224
|
+
accessibilityLabel={closeAccessibilityLabel}
|
|
225
|
+
>
|
|
226
|
+
<Text className={cn('text-lg', config.iconColor)}>{'\u2717'}</Text>
|
|
227
|
+
</Pressable>
|
|
228
|
+
</View>
|
|
229
|
+
</Animated.View>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Hook for managing banner state
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```tsx
|
|
238
|
+
* const { isVisible, bannerProps, showBanner, hideBanner } = useBanner();
|
|
239
|
+
*
|
|
240
|
+
* showBanner({
|
|
241
|
+
* title: 'Success!',
|
|
242
|
+
* description: 'Your changes have been saved.',
|
|
243
|
+
* variant: InfoType.SUCCESS,
|
|
244
|
+
* });
|
|
245
|
+
*
|
|
246
|
+
* {bannerProps && (
|
|
247
|
+
* <Banner
|
|
248
|
+
* isVisible={isVisible}
|
|
249
|
+
* onDismiss={hideBanner}
|
|
250
|
+
* {...bannerProps}
|
|
251
|
+
* />
|
|
252
|
+
* )}
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export interface UseBannerOptions {
|
|
256
|
+
/** Default duration for auto-dismiss (default: 5000) */
|
|
257
|
+
defaultDuration?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface UseBannerReturn {
|
|
261
|
+
/** Whether the banner is currently visible */
|
|
262
|
+
isVisible: boolean;
|
|
263
|
+
/** Current banner props */
|
|
264
|
+
bannerProps: {
|
|
265
|
+
title: string;
|
|
266
|
+
description?: string;
|
|
267
|
+
variant: InfoType;
|
|
268
|
+
duration: number;
|
|
269
|
+
} | null;
|
|
270
|
+
/** Show a banner */
|
|
271
|
+
showBanner: (options: {
|
|
272
|
+
title: string;
|
|
273
|
+
description?: string;
|
|
274
|
+
variant?: InfoType;
|
|
275
|
+
duration?: number;
|
|
276
|
+
}) => void;
|
|
277
|
+
/** Hide the current banner */
|
|
278
|
+
hideBanner: () => void;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const useBanner = (options: UseBannerOptions = {}): UseBannerReturn => {
|
|
282
|
+
const { defaultDuration = 5000 } = options;
|
|
283
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
284
|
+
const [bannerProps, setBannerProps] =
|
|
285
|
+
useState<UseBannerReturn['bannerProps']>(null);
|
|
286
|
+
|
|
287
|
+
const showBanner = useCallback(
|
|
288
|
+
({
|
|
289
|
+
title,
|
|
290
|
+
description,
|
|
291
|
+
variant = InfoType.INFO,
|
|
292
|
+
duration = defaultDuration,
|
|
293
|
+
}: {
|
|
294
|
+
title: string;
|
|
295
|
+
description?: string;
|
|
296
|
+
variant?: InfoType;
|
|
297
|
+
duration?: number;
|
|
298
|
+
}) => {
|
|
299
|
+
setBannerProps({ title, description, variant, duration });
|
|
300
|
+
setIsVisible(true);
|
|
301
|
+
},
|
|
302
|
+
[defaultDuration]
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const hideBanner = useCallback(() => {
|
|
306
|
+
setIsVisible(false);
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
isVisible,
|
|
311
|
+
bannerProps,
|
|
312
|
+
showBanner,
|
|
313
|
+
hideBanner,
|
|
314
|
+
};
|
|
315
|
+
};
|