@bacon-pos/ui 0.3.0 → 0.5.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/lib/commonjs/components/CodeField/CodeField.js +95 -65
- package/lib/commonjs/components/CodeField/CodeField.js.map +1 -1
- package/lib/commonjs/components/CodeField/CodeField.stories.js +24 -1
- package/lib/commonjs/components/CodeField/CodeField.stories.js.map +1 -1
- package/lib/commonjs/components/CodeField/CodeField.test.js +28 -0
- package/lib/commonjs/components/CodeField/CodeField.test.js.map +1 -1
- package/lib/commonjs/components/Dialog/Dialog.js +23 -4
- package/lib/commonjs/components/Dialog/Dialog.js.map +1 -1
- package/lib/commonjs/components/Dialog/Dialog.stories.js +49 -1
- package/lib/commonjs/components/Dialog/Dialog.stories.js.map +1 -1
- package/lib/commonjs/components/Keypad/Keypad.js +162 -0
- package/lib/commonjs/components/Keypad/Keypad.js.map +1 -0
- package/lib/commonjs/components/Keypad/Keypad.stories.js +210 -0
- package/lib/commonjs/components/Keypad/Keypad.stories.js.map +1 -0
- package/lib/commonjs/components/Keypad/Keypad.test.js +119 -0
- package/lib/commonjs/components/Keypad/Keypad.test.js.map +1 -0
- package/lib/commonjs/components/Keypad/index.js +13 -0
- package/lib/commonjs/components/Keypad/index.js.map +1 -0
- package/lib/commonjs/components/Toast/Toast.js +184 -0
- package/lib/commonjs/components/Toast/Toast.js.map +1 -0
- package/lib/commonjs/components/Toast/Toast.stories.js +109 -0
- package/lib/commonjs/components/Toast/Toast.stories.js.map +1 -0
- package/lib/commonjs/components/Toast/Toast.test.js +70 -0
- package/lib/commonjs/components/Toast/Toast.test.js.map +1 -0
- package/lib/commonjs/components/Toast/ToastProvider.js +95 -0
- package/lib/commonjs/components/Toast/ToastProvider.js.map +1 -0
- package/lib/commonjs/components/Toast/index.js +26 -0
- package/lib/commonjs/components/Toast/index.js.map +1 -0
- package/lib/commonjs/components/index.js +30 -8
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.js +95 -65
- package/lib/module/components/CodeField/CodeField.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.stories.js +23 -0
- package/lib/module/components/CodeField/CodeField.stories.js.map +1 -1
- package/lib/module/components/CodeField/CodeField.test.js +28 -0
- package/lib/module/components/CodeField/CodeField.test.js.map +1 -1
- package/lib/module/components/Dialog/Dialog.js +23 -4
- package/lib/module/components/Dialog/Dialog.js.map +1 -1
- package/lib/module/components/Dialog/Dialog.stories.js +48 -0
- package/lib/module/components/Dialog/Dialog.stories.js.map +1 -1
- package/lib/module/components/Keypad/Keypad.js +158 -0
- package/lib/module/components/Keypad/Keypad.js.map +1 -0
- package/lib/module/components/Keypad/Keypad.stories.js +206 -0
- package/lib/module/components/Keypad/Keypad.stories.js.map +1 -0
- package/lib/module/components/Keypad/Keypad.test.js +119 -0
- package/lib/module/components/Keypad/Keypad.test.js.map +1 -0
- package/lib/module/components/Keypad/index.js +4 -0
- package/lib/module/components/Keypad/index.js.map +1 -0
- package/lib/module/components/Toast/Toast.js +180 -0
- package/lib/module/components/Toast/Toast.js.map +1 -0
- package/lib/module/components/Toast/Toast.stories.js +105 -0
- package/lib/module/components/Toast/Toast.stories.js.map +1 -0
- package/lib/module/components/Toast/Toast.test.js +69 -0
- package/lib/module/components/Toast/Toast.test.js.map +1 -0
- package/lib/module/components/Toast/ToastProvider.js +90 -0
- package/lib/module/components/Toast/ToastProvider.js.map +1 -0
- package/lib/module/components/Toast/index.js +5 -0
- package/lib/module/components/Toast/index.js.map +1 -0
- package/lib/module/components/index.js +2 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts +5 -1
- package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +5 -1
- package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts +54 -0
- package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Keypad/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Keypad/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Toast/Toast.d.ts +40 -0
- package/lib/typescript/commonjs/components/Toast/Toast.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Toast/ToastProvider.d.ts +34 -0
- package/lib/typescript/commonjs/components/Toast/ToastProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Toast/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Toast/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +2 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/CodeField/CodeField.d.ts +5 -1
- package/lib/typescript/module/components/CodeField/CodeField.d.ts.map +1 -1
- package/lib/typescript/module/components/Dialog/Dialog.d.ts +5 -1
- package/lib/typescript/module/components/Dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/components/Keypad/Keypad.d.ts +54 -0
- package/lib/typescript/module/components/Keypad/Keypad.d.ts.map +1 -0
- package/lib/typescript/module/components/Keypad/index.d.ts +3 -0
- package/lib/typescript/module/components/Keypad/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Toast/Toast.d.ts +40 -0
- package/lib/typescript/module/components/Toast/Toast.d.ts.map +1 -0
- package/lib/typescript/module/components/Toast/ToastProvider.d.ts +34 -0
- package/lib/typescript/module/components/Toast/ToastProvider.d.ts.map +1 -0
- package/lib/typescript/module/components/Toast/index.d.ts +3 -0
- package/lib/typescript/module/components/Toast/index.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +2 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CodeField/CodeField.tsx +104 -64
- package/src/components/Dialog/Dialog.tsx +40 -10
- package/src/components/Keypad/Keypad.tsx +191 -0
- package/src/components/Keypad/index.ts +2 -0
- package/src/components/Toast/Toast.tsx +173 -0
- package/src/components/Toast/ToastProvider.tsx +126 -0
- package/src/components/Toast/index.ts +7 -0
- package/src/components/index.ts +2 -0
|
@@ -74,6 +74,10 @@ export interface DialogProps {
|
|
|
74
74
|
radius?: number;
|
|
75
75
|
/** Max width of the dialog card. Defaults to 560. */
|
|
76
76
|
maxWidth?: number;
|
|
77
|
+
/** Fixed card width (overrides `maxWidth`). */
|
|
78
|
+
width?: number;
|
|
79
|
+
/** Fill the whole screen — for small devices. Overrides width/maxWidth/scrollMode. */
|
|
80
|
+
fullscreen?: boolean;
|
|
77
81
|
/** Fraction of screen height the card may occupy (inner mode). Defaults to 0.9. */
|
|
78
82
|
maxHeightRatio?: number;
|
|
79
83
|
/** Scroll indicator color (a design-system color). Defaults to a subtle neutral. */
|
|
@@ -108,6 +112,8 @@ export function Dialog({
|
|
|
108
112
|
contentSpacing = 24,
|
|
109
113
|
radius = 24,
|
|
110
114
|
maxWidth = 560,
|
|
115
|
+
width,
|
|
116
|
+
fullscreen = false,
|
|
111
117
|
maxHeightRatio = 0.9,
|
|
112
118
|
scrollbarColor,
|
|
113
119
|
style,
|
|
@@ -185,16 +191,38 @@ export function Dialog({
|
|
|
185
191
|
</View>
|
|
186
192
|
) : null;
|
|
187
193
|
|
|
188
|
-
const cardBase: ViewStyle =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
const cardBase: ViewStyle = fullscreen
|
|
195
|
+
? {
|
|
196
|
+
flex: 1,
|
|
197
|
+
width: '100%',
|
|
198
|
+
backgroundColor: theme.colors.background.default,
|
|
199
|
+
borderRadius: 0,
|
|
200
|
+
}
|
|
201
|
+
: {
|
|
202
|
+
width: width ?? '100%',
|
|
203
|
+
maxWidth: width ? undefined : maxWidth,
|
|
204
|
+
backgroundColor: theme.colors.background.default,
|
|
205
|
+
borderRadius: radius,
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
};
|
|
195
208
|
|
|
196
|
-
const card =
|
|
197
|
-
|
|
209
|
+
const card = fullscreen ? (
|
|
210
|
+
<View accessibilityViewIsModal style={[cardBase, style]}>
|
|
211
|
+
{closeBtn}
|
|
212
|
+
{stickyHeader ? header : null}
|
|
213
|
+
<ScrollArea
|
|
214
|
+
indicatorColor={indicatorColor}
|
|
215
|
+
style={{ flex: 1 }}
|
|
216
|
+
keyboardShouldPersistTaps="handled"
|
|
217
|
+
keyboardDismissMode="interactive"
|
|
218
|
+
>
|
|
219
|
+
{!stickyHeader ? header : null}
|
|
220
|
+
{contentBody}
|
|
221
|
+
{!stickyFooter ? footer : null}
|
|
222
|
+
</ScrollArea>
|
|
223
|
+
{stickyFooter ? footer : null}
|
|
224
|
+
</View>
|
|
225
|
+
) : scrollMode === 'page' ? (
|
|
198
226
|
<View accessibilityViewIsModal style={[cardBase, style]}>
|
|
199
227
|
{closeBtn}
|
|
200
228
|
{header}
|
|
@@ -235,7 +263,9 @@ export function Dialog({
|
|
|
235
263
|
style={{ flex: 1 }}
|
|
236
264
|
>
|
|
237
265
|
<View style={{ flex: 1, backgroundColor: 'rgba(16, 18, 20, 0.55)' }}>
|
|
238
|
-
{
|
|
266
|
+
{fullscreen ? (
|
|
267
|
+
card
|
|
268
|
+
) : scrollMode === 'page' ? (
|
|
239
269
|
<ScrollView
|
|
240
270
|
style={{ flex: 1 }}
|
|
241
271
|
contentContainerStyle={{ flexGrow: 1, padding: 16 }}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { Pressable, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../theme';
|
|
4
|
+
import { Icon } from '../Icon/Icon';
|
|
5
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
6
|
+
import { Text } from '../Text/Text';
|
|
7
|
+
|
|
8
|
+
/** The optional key to the left of 0. Hidden unless provided. */
|
|
9
|
+
export interface KeypadExtraKey {
|
|
10
|
+
/** Short label (e.g. "."). */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Or an icon. */
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
/** If set, pressing appends this to the value (e.g. "." for decimals). */
|
|
15
|
+
value?: string;
|
|
16
|
+
/** Custom action (open a modal, navigate, …). Overrides `value`. */
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
/** Content color. */
|
|
19
|
+
color?: string;
|
|
20
|
+
/** Key background color. */
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KeypadProps {
|
|
25
|
+
/** Controlled value being entered. Defaults to ''. */
|
|
26
|
+
value?: string;
|
|
27
|
+
/** Called with the next value on digit / backspace / extra-value press. */
|
|
28
|
+
onChangeText?: (value: string) => void;
|
|
29
|
+
/** Fired on every press with the raw key: '0'–'9', 'backspace', or 'extra'. */
|
|
30
|
+
onKeyPress?: (key: string) => void;
|
|
31
|
+
/** Max length of `value`. */
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
/** Content shown above the keys (dots, amount, anything). Swappable per use case. */
|
|
34
|
+
header?: ReactNode;
|
|
35
|
+
/** The key left of 0. Hidden by default; provide to show and customize it. */
|
|
36
|
+
extraKey?: KeypadExtraKey;
|
|
37
|
+
/** Key height. Defaults to 83. */
|
|
38
|
+
keyHeight?: number;
|
|
39
|
+
/** Gap between keys. Defaults to 8. */
|
|
40
|
+
gap?: number;
|
|
41
|
+
/** Total width. Defaults to full width; the 3 columns split evenly. */
|
|
42
|
+
width?: number;
|
|
43
|
+
/** Digit font size. Defaults to 24. */
|
|
44
|
+
digitSize?: number;
|
|
45
|
+
/** Space between header and keys. Defaults to 24. */
|
|
46
|
+
headerGap?: number;
|
|
47
|
+
/** Disable all keys. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ROWS = [
|
|
53
|
+
['1', '2', '3'],
|
|
54
|
+
['4', '5', '6'],
|
|
55
|
+
['7', '8', '9'],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Numeric keypad — a 3-column pad decoupled from what it drives (bind it to a
|
|
60
|
+
* PIN display, an amount, etc. via `value`/`onChangeText`, and put that display
|
|
61
|
+
* in `header`). Backspace is fixed to the right of 0; the key left of 0 is hidden
|
|
62
|
+
* unless you pass `extraKey` (custom symbol/icon, action and colors). Width,
|
|
63
|
+
* key height, gap and digit size are all configurable.
|
|
64
|
+
*/
|
|
65
|
+
export function Keypad({
|
|
66
|
+
value = '',
|
|
67
|
+
onChangeText,
|
|
68
|
+
onKeyPress,
|
|
69
|
+
maxLength,
|
|
70
|
+
header,
|
|
71
|
+
extraKey,
|
|
72
|
+
keyHeight = 83,
|
|
73
|
+
gap = 8,
|
|
74
|
+
width,
|
|
75
|
+
digitSize = 24,
|
|
76
|
+
headerGap = 24,
|
|
77
|
+
disabled = false,
|
|
78
|
+
style,
|
|
79
|
+
}: KeypadProps) {
|
|
80
|
+
const theme = useTheme();
|
|
81
|
+
|
|
82
|
+
const keyStyle = (bg?: string): StyleProp<ViewStyle> => ({
|
|
83
|
+
flex: 1,
|
|
84
|
+
height: keyHeight,
|
|
85
|
+
borderRadius: theme.radii.xl,
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: theme.colors.border.strong,
|
|
88
|
+
backgroundColor: bg ?? theme.colors.action.secondary,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const pressDigit = (digit: string) => {
|
|
94
|
+
onKeyPress?.(digit);
|
|
95
|
+
if (maxLength != null && value.length >= maxLength) return;
|
|
96
|
+
onChangeText?.(value + digit);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const pressBackspace = () => {
|
|
100
|
+
onKeyPress?.('backspace');
|
|
101
|
+
onChangeText?.(value.slice(0, -1));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const pressExtra = () => {
|
|
105
|
+
onKeyPress?.('extra');
|
|
106
|
+
if (extraKey?.onPress) {
|
|
107
|
+
extraKey.onPress();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (extraKey?.value != null) {
|
|
111
|
+
if (maxLength != null && value.length >= maxLength) return;
|
|
112
|
+
onChangeText?.(value + extraKey.value);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const DigitKey = ({ digit }: { digit: string }) => (
|
|
117
|
+
<Pressable
|
|
118
|
+
onPress={() => pressDigit(digit)}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
accessibilityRole="button"
|
|
121
|
+
accessibilityLabel={digit}
|
|
122
|
+
style={({ pressed }) => [
|
|
123
|
+
keyStyle(),
|
|
124
|
+
{ opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
<Text
|
|
128
|
+
variant="h2"
|
|
129
|
+
color={theme.colors.action.secondaryForeground}
|
|
130
|
+
style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
|
|
131
|
+
>
|
|
132
|
+
{digit}
|
|
133
|
+
</Text>
|
|
134
|
+
</Pressable>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View style={[{ width: width ?? '100%', gap }, style]}>
|
|
139
|
+
{header != null ? <View style={{ marginBottom: headerGap - gap }}>{header}</View> : null}
|
|
140
|
+
|
|
141
|
+
{ROWS.map((row) => (
|
|
142
|
+
<View key={row[0]} style={{ flexDirection: 'row', gap }}>
|
|
143
|
+
{row.map((digit) => (
|
|
144
|
+
<DigitKey key={digit} digit={digit} />
|
|
145
|
+
))}
|
|
146
|
+
</View>
|
|
147
|
+
))}
|
|
148
|
+
|
|
149
|
+
<View style={{ flexDirection: 'row', gap }}>
|
|
150
|
+
{extraKey ? (
|
|
151
|
+
<Pressable
|
|
152
|
+
onPress={pressExtra}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
accessibilityRole="button"
|
|
155
|
+
accessibilityLabel={extraKey.label ?? 'action'}
|
|
156
|
+
style={({ pressed }) => [
|
|
157
|
+
keyStyle(extraKey.backgroundColor),
|
|
158
|
+
{ opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
159
|
+
]}
|
|
160
|
+
>
|
|
161
|
+
{extraKey.icon ? (
|
|
162
|
+
<Icon name={extraKey.icon} size={24} color={extraKey.color ?? theme.colors.action.secondaryForeground} />
|
|
163
|
+
) : (
|
|
164
|
+
<Text
|
|
165
|
+
variant="h2"
|
|
166
|
+
color={extraKey.color ?? theme.colors.action.secondaryForeground}
|
|
167
|
+
style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
|
|
168
|
+
>
|
|
169
|
+
{extraKey.label ?? ''}
|
|
170
|
+
</Text>
|
|
171
|
+
)}
|
|
172
|
+
</Pressable>
|
|
173
|
+
) : (
|
|
174
|
+
<View style={{ flex: 1 }} />
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<DigitKey digit="0" />
|
|
178
|
+
|
|
179
|
+
<Pressable
|
|
180
|
+
onPress={pressBackspace}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
accessibilityRole="button"
|
|
183
|
+
accessibilityLabel="Backspace"
|
|
184
|
+
style={({ pressed }) => [keyStyle(), { opacity: disabled ? 0.5 : pressed ? 0.85 : 1 }]}
|
|
185
|
+
>
|
|
186
|
+
<Icon name="arrow-left" size={24} color={theme.colors.action.secondaryForeground} />
|
|
187
|
+
</Pressable>
|
|
188
|
+
</View>
|
|
189
|
+
</View>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Pressable,
|
|
6
|
+
type LayoutChangeEvent,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useTheme, type Theme } from '../../theme';
|
|
10
|
+
import { palette } from '../../theme/palette';
|
|
11
|
+
import { Icon } from '../Icon/Icon';
|
|
12
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
13
|
+
import { Text } from '../Text/Text';
|
|
14
|
+
|
|
15
|
+
/** Built-in statuses. More can be added later, or use custom colors. */
|
|
16
|
+
export type ToastStatus = 'success' | 'error' | 'warning' | 'info';
|
|
17
|
+
|
|
18
|
+
export interface ToastOptions {
|
|
19
|
+
/** Message text. Ellipsized on a single line if it overflows. */
|
|
20
|
+
message: string;
|
|
21
|
+
/** Visual status. Defaults to `info`. */
|
|
22
|
+
status?: ToastStatus;
|
|
23
|
+
/** Optional leading icon. */
|
|
24
|
+
icon?: IconName;
|
|
25
|
+
/** Time on screen in ms. `0` keeps it until dismissed. */
|
|
26
|
+
duration?: number;
|
|
27
|
+
/** Custom fixed height. */
|
|
28
|
+
height?: number;
|
|
29
|
+
/** Custom fixed width. Defaults to full width. */
|
|
30
|
+
width?: number;
|
|
31
|
+
/** Show a countdown progress bar for the remaining time (needs duration > 0). */
|
|
32
|
+
showProgress?: boolean;
|
|
33
|
+
/** Custom background (overrides status). */
|
|
34
|
+
backgroundColor?: string;
|
|
35
|
+
/** Custom text/icon color (overrides status). */
|
|
36
|
+
color?: string;
|
|
37
|
+
/** Countdown bar color. Defaults to the status foreground. */
|
|
38
|
+
progressColor?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statusColors(theme: Theme, status: ToastStatus): { bg: string; fg: string } {
|
|
42
|
+
switch (status) {
|
|
43
|
+
case 'success':
|
|
44
|
+
return { bg: theme.colors.feedback.successSubtle, fg: theme.colors.feedback.success };
|
|
45
|
+
case 'error':
|
|
46
|
+
return { bg: theme.colors.feedback.errorSubtle, fg: theme.colors.feedback.error };
|
|
47
|
+
case 'warning':
|
|
48
|
+
return { bg: theme.colors.feedback.warningSubtle, fg: palette.warning[600] };
|
|
49
|
+
case 'info':
|
|
50
|
+
default:
|
|
51
|
+
return { bg: theme.colors.background.subtle, fg: palette.neutral[500] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ToastViewProps extends ToastOptions {
|
|
56
|
+
id: number;
|
|
57
|
+
position: 'top' | 'bottom';
|
|
58
|
+
/** When true, the toast animates out and then calls `onRemove`. */
|
|
59
|
+
leaving: boolean;
|
|
60
|
+
onRequestClose: (id: number) => void;
|
|
61
|
+
onRemove: (id: number) => void;
|
|
62
|
+
maxWidth?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A single toast with an elegant slide + fade in/out. Self-contained: it runs
|
|
67
|
+
* its own duration timer and exit animation. Managed by `ToastProvider`.
|
|
68
|
+
*/
|
|
69
|
+
export function ToastView({
|
|
70
|
+
id,
|
|
71
|
+
message,
|
|
72
|
+
status = 'info',
|
|
73
|
+
icon,
|
|
74
|
+
duration = 3000,
|
|
75
|
+
height,
|
|
76
|
+
width,
|
|
77
|
+
showProgress = false,
|
|
78
|
+
backgroundColor,
|
|
79
|
+
color,
|
|
80
|
+
progressColor,
|
|
81
|
+
position,
|
|
82
|
+
leaving,
|
|
83
|
+
onRequestClose,
|
|
84
|
+
onRemove,
|
|
85
|
+
maxWidth,
|
|
86
|
+
}: ToastViewProps) {
|
|
87
|
+
const theme = useTheme();
|
|
88
|
+
const base = statusColors(theme, status);
|
|
89
|
+
const bg = backgroundColor ?? base.bg;
|
|
90
|
+
const fg = color ?? base.fg;
|
|
91
|
+
|
|
92
|
+
const from = position === 'top' ? -12 : 12;
|
|
93
|
+
const translateY = useRef(new Animated.Value(from)).current;
|
|
94
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
95
|
+
const remaining = useRef(new Animated.Value(1)).current;
|
|
96
|
+
const [contentW, setContentW] = useState(0);
|
|
97
|
+
const withProgress = showProgress && duration > 0;
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
Animated.parallel([
|
|
101
|
+
Animated.timing(opacity, { toValue: 1, duration: 260, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
|
102
|
+
Animated.timing(translateY, { toValue: 0, duration: 260, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
|
103
|
+
]).start();
|
|
104
|
+
if (withProgress) {
|
|
105
|
+
Animated.timing(remaining, { toValue: 0, duration, easing: Easing.linear, useNativeDriver: false }).start();
|
|
106
|
+
}
|
|
107
|
+
if (duration > 0) {
|
|
108
|
+
const timer = setTimeout(() => onRequestClose(id), duration);
|
|
109
|
+
return () => clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
// Mount only.
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!leaving) return;
|
|
118
|
+
Animated.parallel([
|
|
119
|
+
Animated.timing(opacity, { toValue: 0, duration: 200, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
120
|
+
Animated.timing(translateY, { toValue: from, duration: 200, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
121
|
+
]).start(() => onRemove(id));
|
|
122
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
+
}, [leaving]);
|
|
124
|
+
|
|
125
|
+
const pressStyle: ViewStyle = {
|
|
126
|
+
flexDirection: 'row',
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
justifyContent: 'center',
|
|
129
|
+
gap: 12,
|
|
130
|
+
paddingHorizontal: 20,
|
|
131
|
+
paddingVertical: height ? 0 : 20,
|
|
132
|
+
borderRadius: 16,
|
|
133
|
+
backgroundColor: bg,
|
|
134
|
+
overflow: 'hidden',
|
|
135
|
+
...(height ? { height } : null),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Fixed width if given, otherwise full width (capped by the provider maxWidth).
|
|
139
|
+
const outerWidth: ViewStyle = width != null ? { width } : { width: '100%', maxWidth };
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Animated.View
|
|
143
|
+
accessibilityRole="alert"
|
|
144
|
+
accessibilityLiveRegion="polite"
|
|
145
|
+
style={{ opacity, transform: [{ translateY }], ...outerWidth }}
|
|
146
|
+
>
|
|
147
|
+
<Pressable
|
|
148
|
+
onPress={() => onRequestClose(id)}
|
|
149
|
+
onLayout={(e: LayoutChangeEvent) => setContentW(e.nativeEvent.layout.width)}
|
|
150
|
+
style={pressStyle}
|
|
151
|
+
>
|
|
152
|
+
{icon ? <Icon name={icon} size={24} color={fg} /> : null}
|
|
153
|
+
<Text variant="bodyLarge" weight="medium" color={fg} numberOfLines={1} align="center" style={{ flexShrink: 1 }}>
|
|
154
|
+
{message}
|
|
155
|
+
</Text>
|
|
156
|
+
{withProgress ? (
|
|
157
|
+
<Animated.View
|
|
158
|
+
pointerEvents="none"
|
|
159
|
+
style={{
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
left: 0,
|
|
162
|
+
bottom: 0,
|
|
163
|
+
height: 3,
|
|
164
|
+
borderRadius: 3,
|
|
165
|
+
backgroundColor: progressColor ?? fg,
|
|
166
|
+
width: remaining.interpolate({ inputRange: [0, 1], outputRange: [0, contentW] }),
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
) : null}
|
|
170
|
+
</Pressable>
|
|
171
|
+
</Animated.View>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { View } from 'react-native';
|
|
11
|
+
import { ToastView, type ToastOptions } from './Toast';
|
|
12
|
+
|
|
13
|
+
export interface ToastApi {
|
|
14
|
+
/** Show a toast; returns its id. */
|
|
15
|
+
show: (options: ToastOptions) => number;
|
|
16
|
+
/** Dismiss a toast by id (animates out). */
|
|
17
|
+
hide: (id: number) => void;
|
|
18
|
+
/** Dismiss all toasts. */
|
|
19
|
+
hideAll: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ToastContext = createContext<ToastApi | null>(null);
|
|
23
|
+
|
|
24
|
+
interface ToastItem extends ToastOptions {
|
|
25
|
+
id: number;
|
|
26
|
+
leaving: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToastProviderProps {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
/** Where toasts appear. Defaults to `bottom`. */
|
|
32
|
+
position?: 'top' | 'bottom';
|
|
33
|
+
/** Max toasts on screen at once. Defaults to 2. */
|
|
34
|
+
maxVisible?: number;
|
|
35
|
+
/** Default time on screen in ms. Defaults to 3000. */
|
|
36
|
+
duration?: number;
|
|
37
|
+
/** Distance from the screen edge. Defaults to 24. */
|
|
38
|
+
offset?: number;
|
|
39
|
+
/** Gap between stacked toasts. Defaults to 8. */
|
|
40
|
+
gap?: number;
|
|
41
|
+
/** Max toast width. Defaults to full width (no cap). */
|
|
42
|
+
maxWidth?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Provides the imperative toast API and renders the toast stack. Wrap your app
|
|
47
|
+
* (inside ThemeProvider). Trigger toasts with `useToast().show(...)`. Newest
|
|
48
|
+
* toasts appear nearest the edge; at most `maxVisible` show at once.
|
|
49
|
+
*/
|
|
50
|
+
export function ToastProvider({
|
|
51
|
+
children,
|
|
52
|
+
position = 'bottom',
|
|
53
|
+
maxVisible = 2,
|
|
54
|
+
duration = 3000,
|
|
55
|
+
offset = 24,
|
|
56
|
+
gap = 8,
|
|
57
|
+
maxWidth,
|
|
58
|
+
}: ToastProviderProps) {
|
|
59
|
+
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
|
60
|
+
const idRef = useRef(0);
|
|
61
|
+
|
|
62
|
+
const show = useCallback(
|
|
63
|
+
(options: ToastOptions) => {
|
|
64
|
+
const id = (idRef.current += 1);
|
|
65
|
+
setToasts((prev) => {
|
|
66
|
+
const next: ToastItem[] = [...prev, { leaving: false, duration, ...options, id }];
|
|
67
|
+
return next.length > maxVisible ? next.slice(next.length - maxVisible) : next;
|
|
68
|
+
});
|
|
69
|
+
return id;
|
|
70
|
+
},
|
|
71
|
+
[duration, maxVisible],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const hide = useCallback((id: number) => {
|
|
75
|
+
setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, leaving: true } : t)));
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const remove = useCallback((id: number) => {
|
|
79
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const hideAll = useCallback(() => {
|
|
83
|
+
setToasts((prev) => prev.map((t) => ({ ...t, leaving: true })));
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const api = useMemo<ToastApi>(() => ({ show, hide, hideAll }), [show, hide, hideAll]);
|
|
87
|
+
|
|
88
|
+
// Newest nearest the edge.
|
|
89
|
+
const display = position === 'top' ? [...toasts].reverse() : toasts;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ToastContext.Provider value={api}>
|
|
93
|
+
{children}
|
|
94
|
+
<View
|
|
95
|
+
pointerEvents="box-none"
|
|
96
|
+
style={{
|
|
97
|
+
position: 'absolute',
|
|
98
|
+
left: 0,
|
|
99
|
+
right: 0,
|
|
100
|
+
[position]: offset,
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
paddingHorizontal: 16,
|
|
103
|
+
gap,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{display.map((t) => (
|
|
107
|
+
<ToastView
|
|
108
|
+
key={t.id}
|
|
109
|
+
{...t}
|
|
110
|
+
position={position}
|
|
111
|
+
maxWidth={maxWidth}
|
|
112
|
+
onRequestClose={hide}
|
|
113
|
+
onRemove={remove}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
</ToastContext.Provider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Access the toast API. Must be used within a `ToastProvider`. */
|
|
122
|
+
export function useToast(): ToastApi {
|
|
123
|
+
const ctx = useContext(ToastContext);
|
|
124
|
+
if (!ctx) throw new Error('useToast must be used within a ToastProvider');
|
|
125
|
+
return ctx;
|
|
126
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './Dialog';
|
|
|
10
10
|
export * from './Icon';
|
|
11
11
|
export * from './IconButton';
|
|
12
12
|
export * from './Input';
|
|
13
|
+
export * from './Keypad';
|
|
13
14
|
export * from './LinkButton';
|
|
14
15
|
export * from './ProductCard';
|
|
15
16
|
export * from './QuantityStepper';
|
|
@@ -18,5 +19,6 @@ export * from './Spinner';
|
|
|
18
19
|
export * from './Surface';
|
|
19
20
|
export * from './Switch';
|
|
20
21
|
export * from './Text';
|
|
22
|
+
export * from './Toast';
|
|
21
23
|
|
|
22
24
|
export type { LabelPosition } from './_internal/Labeled';
|