@oxyhq/bloom 0.4.0 → 0.5.1
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.md +105 -90
- package/lib/commonjs/bottom-sheet/index.js +2 -2
- package/lib/commonjs/context-menu/index.js +18 -19
- package/lib/commonjs/context-menu/index.js.map +1 -1
- package/lib/commonjs/dialog/BloomDialogProvider.js +61 -0
- package/lib/commonjs/dialog/BloomDialogProvider.js.map +1 -0
- package/lib/commonjs/dialog/BloomDialogProvider.web.js +45 -0
- package/lib/commonjs/dialog/BloomDialogProvider.web.js.map +1 -0
- package/lib/commonjs/dialog/Dialog.js +197 -100
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +194 -84
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/SheetShell.js +149 -0
- package/lib/commonjs/dialog/SheetShell.js.map +1 -0
- package/lib/commonjs/dialog/alert-store.js +116 -0
- package/lib/commonjs/dialog/alert-store.js.map +1 -0
- package/lib/commonjs/dialog/alert.js +38 -0
- package/lib/commonjs/dialog/alert.js.map +1 -0
- package/lib/commonjs/dialog/context.js +10 -2
- package/lib/commonjs/dialog/context.js.map +1 -1
- package/lib/commonjs/dialog/index.js +8 -24
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +10 -20
- package/lib/commonjs/dialog/index.web.js.map +1 -1
- package/lib/commonjs/fonts/FontLoader.js +6 -5
- package/lib/commonjs/fonts/FontLoader.js.map +1 -1
- package/lib/commonjs/fonts/apply-font-faces.js +4 -4
- package/lib/commonjs/fonts/apply-font-faces.web.js +13 -12
- package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/commonjs/fonts/font-assets.js +2 -2
- package/lib/commonjs/fonts/font-data.web.js +22 -0
- package/lib/commonjs/fonts/font-data.web.js.map +1 -0
- package/lib/commonjs/index.js +101 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +101 -66
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/menu/index.js +21 -23
- package/lib/commonjs/menu/index.js.map +1 -1
- package/lib/commonjs/select/index.js +26 -27
- package/lib/commonjs/select/index.js.map +1 -1
- package/lib/commonjs/toast/index.js +42 -13
- package/lib/commonjs/toast/index.js.map +1 -1
- package/lib/commonjs/toast/index.web.js +19 -15
- package/lib/commonjs/toast/index.web.js.map +1 -1
- package/lib/module/bottom-sheet/index.js +2 -2
- package/lib/module/context-menu/index.js +15 -16
- package/lib/module/context-menu/index.js.map +1 -1
- package/lib/module/dialog/BloomDialogProvider.js +57 -0
- package/lib/module/dialog/BloomDialogProvider.js.map +1 -0
- package/lib/module/dialog/BloomDialogProvider.web.js +41 -0
- package/lib/module/dialog/BloomDialogProvider.web.js.map +1 -0
- package/lib/module/dialog/Dialog.js +199 -87
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +195 -70
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/SheetShell.js +143 -0
- package/lib/module/dialog/SheetShell.js.map +1 -0
- package/lib/module/dialog/alert-store.js +107 -0
- package/lib/module/dialog/alert-store.js.map +1 -0
- package/lib/module/dialog/alert.js +35 -0
- package/lib/module/dialog/alert.js.map +1 -0
- package/lib/module/dialog/context.js +10 -2
- package/lib/module/dialog/context.js.map +1 -1
- package/lib/module/dialog/index.js +3 -1
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +9 -7
- package/lib/module/dialog/index.web.js.map +1 -1
- package/lib/module/fonts/FontLoader.js +6 -5
- package/lib/module/fonts/FontLoader.js.map +1 -1
- package/lib/module/fonts/apply-font-faces.js +4 -4
- package/lib/module/fonts/apply-font-faces.web.js +13 -10
- package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/module/fonts/font-assets.js +2 -2
- package/lib/module/fonts/font-data.web.js +18 -0
- package/lib/module/fonts/font-data.web.js.map +1 -0
- package/lib/module/fonts/index.web.js +4 -4
- package/lib/module/index.js +2 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -3
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/menu/index.js +11 -13
- package/lib/module/menu/index.js.map +1 -1
- package/lib/module/select/index.js +27 -28
- package/lib/module/select/index.js.map +1 -1
- package/lib/module/toast/index.js +41 -11
- package/lib/module/toast/index.js.map +1 -1
- package/lib/module/toast/index.web.js +18 -13
- package/lib/module/toast/index.web.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts +1 -1
- package/lib/typescript/commonjs/context-menu/index.d.ts +4 -3
- package/lib/typescript/commonjs/context-menu/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/BloomDialogProvider.d.ts +27 -0
- package/lib/typescript/commonjs/dialog/BloomDialogProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/BloomDialogProvider.web.d.ts +15 -0
- package/lib/typescript/commonjs/dialog/BloomDialogProvider.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/Dialog.d.ts +37 -10
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +26 -10
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/SheetShell.d.ts +31 -0
- package/lib/typescript/commonjs/dialog/SheetShell.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/alert-store.d.ts +70 -0
- package/lib/typescript/commonjs/dialog/alert-store.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/alert.d.ts +27 -0
- package/lib/typescript/commonjs/dialog/alert.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/context.d.ts +7 -0
- package/lib/typescript/commonjs/dialog/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.d.ts +5 -2
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +5 -2
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +70 -15
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/FontLoader.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +8 -1
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/font-data.web.d.ts +5 -0
- package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +3 -3
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/menu/index.d.ts +4 -4
- package/lib/typescript/commonjs/menu/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/index.d.ts +32 -3
- package/lib/typescript/commonjs/toast/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/index.web.d.ts +14 -7
- package/lib/typescript/commonjs/toast/index.web.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.d.ts +1 -1
- package/lib/typescript/module/context-menu/index.d.ts +4 -3
- package/lib/typescript/module/context-menu/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/BloomDialogProvider.d.ts +27 -0
- package/lib/typescript/module/dialog/BloomDialogProvider.d.ts.map +1 -0
- package/lib/typescript/module/dialog/BloomDialogProvider.web.d.ts +15 -0
- package/lib/typescript/module/dialog/BloomDialogProvider.web.d.ts.map +1 -0
- package/lib/typescript/module/dialog/Dialog.d.ts +37 -10
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts +26 -10
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/SheetShell.d.ts +31 -0
- package/lib/typescript/module/dialog/SheetShell.d.ts.map +1 -0
- package/lib/typescript/module/dialog/alert-store.d.ts +70 -0
- package/lib/typescript/module/dialog/alert-store.d.ts.map +1 -0
- package/lib/typescript/module/dialog/alert.d.ts +27 -0
- package/lib/typescript/module/dialog/alert.d.ts.map +1 -0
- package/lib/typescript/module/dialog/context.d.ts +7 -0
- package/lib/typescript/module/dialog/context.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.d.ts +5 -2
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +5 -2
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +70 -15
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/fonts/FontLoader.d.ts.map +1 -1
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +8 -1
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/module/fonts/font-data.web.d.ts +5 -0
- package/lib/typescript/module/fonts/font-data.web.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +3 -3
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/menu/index.d.ts +4 -4
- package/lib/typescript/module/menu/index.d.ts.map +1 -1
- package/lib/typescript/module/select/index.d.ts.map +1 -1
- package/lib/typescript/module/toast/index.d.ts +32 -3
- package/lib/typescript/module/toast/index.d.ts.map +1 -1
- package/lib/typescript/module/toast/index.web.d.ts +14 -7
- package/lib/typescript/module/toast/index.web.d.ts.map +1 -1
- package/package.json +38 -18
- package/src/__tests__/Dialog.test.tsx +177 -0
- package/src/avatar/Avatar.stories.tsx +69 -0
- package/src/bottom-sheet/BottomSheet.stories.tsx +92 -0
- package/src/bottom-sheet/index.tsx +3 -3
- package/src/button/Button.stories.tsx +94 -0
- package/src/context-menu/ContextMenu.stories.tsx +71 -0
- package/src/context-menu/index.tsx +12 -12
- package/src/dialog/BloomDialogProvider.tsx +61 -0
- package/src/dialog/BloomDialogProvider.web.tsx +46 -0
- package/src/dialog/Dialog.stories.tsx +112 -0
- package/src/dialog/Dialog.tsx +217 -64
- package/src/dialog/Dialog.web.tsx +240 -75
- package/src/dialog/SheetShell.tsx +154 -0
- package/src/dialog/alert-store.ts +126 -0
- package/src/dialog/alert.ts +42 -0
- package/src/dialog/context.ts +14 -3
- package/src/dialog/index.ts +14 -2
- package/src/dialog/index.web.ts +20 -8
- package/src/dialog/types.ts +73 -16
- package/src/fonts/FontLoader.tsx +6 -5
- package/src/fonts/apply-font-faces.ts +4 -4
- package/src/fonts/apply-font-faces.web.ts +18 -10
- package/src/fonts/font-assets.ts +2 -2
- package/src/fonts/font-data.web.ts +15 -0
- package/src/fonts/index.web.ts +4 -4
- package/src/index.ts +17 -3
- package/src/index.web.ts +17 -3
- package/src/loading/Loading.stories.tsx +60 -0
- package/src/menu/Menu.stories.tsx +79 -0
- package/src/menu/index.tsx +13 -17
- package/src/prompt-input/PromptInput.stories.tsx +82 -0
- package/src/select/Select.stories.tsx +84 -0
- package/src/select/index.tsx +30 -30
- package/src/settings-list/SettingsList.stories.tsx +106 -0
- package/src/text-field/TextField.stories.tsx +90 -0
- package/src/toast/Toast.stories.tsx +109 -0
- package/src/toast/index.tsx +55 -11
- package/src/toast/index.web.tsx +33 -13
- package/lib/commonjs/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
- package/lib/commonjs/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
- package/lib/commonjs/fonts/assets/GeistMono-Variable.woff2 +0 -0
- package/lib/commonjs/fonts/assets/InterVariable.woff2 +0 -0
- package/lib/commonjs/prompt/Prompt.js +0 -267
- package/lib/commonjs/prompt/Prompt.js.map +0 -1
- package/lib/commonjs/prompt/index.js +0 -61
- package/lib/commonjs/prompt/index.js.map +0 -1
- package/lib/module/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
- package/lib/module/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
- package/lib/module/fonts/assets/GeistMono-Variable.woff2 +0 -0
- package/lib/module/fonts/assets/InterVariable.woff2 +0 -0
- package/lib/module/prompt/Prompt.js +0 -250
- package/lib/module/prompt/Prompt.js.map +0 -1
- package/lib/module/prompt/index.js +0 -4
- package/lib/module/prompt/index.js.map +0 -1
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/Button.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/Code.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/Code.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/Pre.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/Pre.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts +0 -5
- package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/__tests__/theme.test.d.ts +0 -2
- package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +0 -1
- package/lib/typescript/commonjs/prompt/Prompt.d.ts +0 -42
- package/lib/typescript/commonjs/prompt/Prompt.d.ts.map +0 -1
- package/lib/typescript/commonjs/prompt/index.d.ts +0 -3
- package/lib/typescript/commonjs/prompt/index.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
- package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/Button.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/Button.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/Code.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/Code.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/Pre.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/Pre.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/SettingsList.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/SettingsList.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts +0 -5
- package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts.map +0 -1
- package/lib/typescript/module/__tests__/theme.test.d.ts +0 -2
- package/lib/typescript/module/__tests__/theme.test.d.ts.map +0 -1
- package/lib/typescript/module/prompt/Prompt.d.ts +0 -42
- package/lib/typescript/module/prompt/Prompt.d.ts.map +0 -1
- package/lib/typescript/module/prompt/index.d.ts +0 -3
- package/lib/typescript/module/prompt/index.d.ts.map +0 -1
- package/src/prompt/Prompt.tsx +0 -247
- package/src/prompt/index.ts +0 -13
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
Pressable,
|
|
14
|
+
Text,
|
|
15
|
+
TouchableOpacity,
|
|
16
|
+
View,
|
|
17
|
+
type GestureResponderEvent,
|
|
18
|
+
type ViewStyle,
|
|
19
|
+
} from 'react-native';
|
|
3
20
|
import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
4
21
|
|
|
5
|
-
import { useTheme } from '../theme/use-theme';
|
|
6
22
|
import { Portal } from '../portal/index.web';
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
import type { ThemeColors } from '../theme/types';
|
|
24
|
+
import { useTheme } from '../theme/use-theme';
|
|
25
|
+
import { Context, useDialogContext, useDialogControl } from './context';
|
|
26
|
+
import type {
|
|
27
|
+
DialogAction,
|
|
28
|
+
DialogActionColor,
|
|
29
|
+
DialogControlProps,
|
|
30
|
+
DialogProps,
|
|
31
|
+
} from './types';
|
|
12
32
|
|
|
13
33
|
const FADE_OUT_DURATION = 150;
|
|
14
34
|
|
|
@@ -16,13 +36,30 @@ const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagatio
|
|
|
16
36
|
|
|
17
37
|
const ClosingContext = createContext(false);
|
|
18
38
|
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Web variant of `<Dialog>`.
|
|
41
|
+
*
|
|
42
|
+
* A centered modal card rendered into the bloom Portal at the end of
|
|
43
|
+
* `document.body`. Same prop API as native — `title`, `description`,
|
|
44
|
+
* `actions`, or arbitrary `children` — so call sites are platform
|
|
45
|
+
* agnostic.
|
|
46
|
+
*
|
|
47
|
+
* Accessibility: the panel has `role="dialog"` and the `title`/`description`
|
|
48
|
+
* props (when provided) become the `aria-labelledby` / `aria-describedby`
|
|
49
|
+
* targets. Pressing the backdrop dismisses. Pressing `Escape` dismisses.
|
|
50
|
+
* Focus is locked inside the dialog while it's open.
|
|
51
|
+
*/
|
|
52
|
+
export function Dialog({
|
|
21
53
|
control,
|
|
22
54
|
onClose,
|
|
23
55
|
testID,
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
title,
|
|
57
|
+
description,
|
|
58
|
+
actions,
|
|
59
|
+
style,
|
|
60
|
+
label,
|
|
61
|
+
children,
|
|
62
|
+
}: DialogProps) {
|
|
26
63
|
const [isOpen, setIsOpen] = useState(false);
|
|
27
64
|
const [isClosing, setIsClosing] = useState(false);
|
|
28
65
|
const closeCallbacksRef = useRef<(() => void)[]>([]);
|
|
@@ -45,20 +82,38 @@ export function Outer({
|
|
|
45
82
|
const timer = setTimeout(() => {
|
|
46
83
|
setIsOpen(false);
|
|
47
84
|
setIsClosing(false);
|
|
48
|
-
|
|
85
|
+
const queued = closeCallbacksRef.current;
|
|
86
|
+
closeCallbacksRef.current = [];
|
|
87
|
+
for (const cb of queued) {
|
|
49
88
|
try {
|
|
50
89
|
cb();
|
|
51
90
|
} catch (e) {
|
|
52
|
-
|
|
91
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
92
|
+
console.error('Dialog close callback error:', e);
|
|
93
|
+
}
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
|
-
closeCallbacksRef.current = [];
|
|
56
96
|
onClose?.();
|
|
57
97
|
}, FADE_OUT_DURATION);
|
|
58
98
|
|
|
59
99
|
return () => clearTimeout(timer);
|
|
60
100
|
}, [isClosing, onClose]);
|
|
61
101
|
|
|
102
|
+
// Escape-to-close while open. The listener is intentionally scoped to the
|
|
103
|
+
// open lifetime so stacked dialogs don't fight for the keydown — the
|
|
104
|
+
// top-most one wins via document-level event order.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!isOpen || typeof document === 'undefined') return;
|
|
107
|
+
const handler = (e: KeyboardEvent) => {
|
|
108
|
+
if (e.key === 'Escape') {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
close();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener('keydown', handler);
|
|
114
|
+
return () => document.removeEventListener('keydown', handler);
|
|
115
|
+
}, [close, isOpen]);
|
|
116
|
+
|
|
62
117
|
useImperativeHandle(
|
|
63
118
|
control.ref,
|
|
64
119
|
() => ({ open, close }),
|
|
@@ -87,24 +142,22 @@ export function Outer({
|
|
|
87
142
|
bottom: 0,
|
|
88
143
|
zIndex: 50,
|
|
89
144
|
alignItems: 'center',
|
|
90
|
-
justifyContent:
|
|
145
|
+
justifyContent: 'center',
|
|
91
146
|
paddingHorizontal: 20,
|
|
92
|
-
paddingVertical: '10vh' as unknown as number,
|
|
93
|
-
...({ overflowY: 'auto' } as Record<string, string>),
|
|
94
147
|
}}
|
|
95
148
|
>
|
|
96
149
|
<DialogBackdrop isClosing={isClosing} />
|
|
97
|
-
<
|
|
150
|
+
<DialogPanel
|
|
98
151
|
testID={testID}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
152
|
+
label={label}
|
|
153
|
+
title={title}
|
|
154
|
+
description={description}
|
|
155
|
+
actions={actions}
|
|
156
|
+
style={style}
|
|
157
|
+
isClosing={isClosing}
|
|
105
158
|
>
|
|
106
159
|
{children}
|
|
107
|
-
</
|
|
160
|
+
</DialogPanel>
|
|
108
161
|
</Pressable>
|
|
109
162
|
</ClosingContext.Provider>
|
|
110
163
|
</Context.Provider>
|
|
@@ -112,29 +165,45 @@ export function Outer({
|
|
|
112
165
|
);
|
|
113
166
|
}
|
|
114
167
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
style,
|
|
168
|
+
function DialogPanel({
|
|
169
|
+
testID,
|
|
118
170
|
label,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
title,
|
|
172
|
+
description,
|
|
173
|
+
actions,
|
|
174
|
+
style,
|
|
175
|
+
isClosing,
|
|
176
|
+
children,
|
|
177
|
+
}: {
|
|
178
|
+
testID?: string;
|
|
179
|
+
label?: string;
|
|
180
|
+
title?: string;
|
|
181
|
+
description?: string;
|
|
182
|
+
actions?: DialogAction[];
|
|
183
|
+
style?: DialogProps['style'];
|
|
184
|
+
isClosing: boolean;
|
|
185
|
+
children?: React.ReactNode;
|
|
186
|
+
}) {
|
|
122
187
|
const theme = useTheme();
|
|
123
|
-
const
|
|
188
|
+
const titleId = useId();
|
|
189
|
+
const descriptionId = useId();
|
|
124
190
|
|
|
125
191
|
return (
|
|
126
192
|
<View
|
|
127
193
|
role="dialog"
|
|
128
194
|
aria-label={label}
|
|
195
|
+
aria-labelledby={title ? titleId : undefined}
|
|
196
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
197
|
+
testID={testID}
|
|
129
198
|
onStartShouldSetResponder={() => true}
|
|
130
199
|
onResponderRelease={stopPropagation}
|
|
131
200
|
{...({ onClick: stopPropagation } as Record<string, unknown>)}
|
|
132
201
|
style={[
|
|
133
202
|
{
|
|
134
203
|
position: 'relative',
|
|
135
|
-
borderRadius:
|
|
204
|
+
borderRadius: 20,
|
|
136
205
|
width: '100%',
|
|
137
|
-
maxWidth:
|
|
206
|
+
maxWidth: 480,
|
|
138
207
|
backgroundColor: theme.colors.background,
|
|
139
208
|
borderWidth: 1,
|
|
140
209
|
borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
|
@@ -142,63 +211,130 @@ export function Inner({
|
|
|
142
211
|
shadowOpacity: theme.isDark ? 0.4 : 0.1,
|
|
143
212
|
shadowRadius: 30,
|
|
144
213
|
shadowOffset: { width: 0, height: 4 },
|
|
145
|
-
|
|
214
|
+
padding: 20,
|
|
215
|
+
zIndex: 60,
|
|
146
216
|
},
|
|
147
217
|
isClosing
|
|
148
|
-
? { animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle
|
|
149
|
-
: { animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s' } as ViewStyle,
|
|
218
|
+
? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
|
|
219
|
+
: ({ animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s' } as ViewStyle),
|
|
150
220
|
style,
|
|
151
221
|
]}
|
|
152
222
|
>
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
223
|
+
{title ? (
|
|
224
|
+
<Text
|
|
225
|
+
nativeID={titleId}
|
|
226
|
+
style={{
|
|
227
|
+
fontSize: 22,
|
|
228
|
+
fontWeight: '600',
|
|
229
|
+
color: theme.colors.text,
|
|
230
|
+
paddingBottom: description ? 4 : 16,
|
|
231
|
+
lineHeight: 30,
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{title}
|
|
235
|
+
</Text>
|
|
236
|
+
) : null}
|
|
237
|
+
{description ? (
|
|
238
|
+
<Text
|
|
239
|
+
nativeID={descriptionId}
|
|
240
|
+
style={{
|
|
241
|
+
fontSize: 16,
|
|
242
|
+
color: theme.colors.textSecondary,
|
|
243
|
+
paddingBottom: 16,
|
|
244
|
+
lineHeight: 22,
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{description}
|
|
248
|
+
</Text>
|
|
249
|
+
) : null}
|
|
250
|
+
{children}
|
|
251
|
+
{actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
|
|
157
252
|
</View>
|
|
158
253
|
);
|
|
159
254
|
}
|
|
160
255
|
|
|
161
|
-
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
function ActionRow({ actions }: { actions: DialogAction[] }) {
|
|
257
|
+
return (
|
|
258
|
+
<View style={{ width: '100%', gap: 8, justifyContent: 'flex-end' }}>
|
|
259
|
+
{actions.map((action, idx) => (
|
|
260
|
+
<ActionButton
|
|
261
|
+
key={`${action.label}-${idx}`}
|
|
262
|
+
action={action}
|
|
263
|
+
/>
|
|
264
|
+
))}
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
167
267
|
}
|
|
168
268
|
|
|
169
|
-
|
|
269
|
+
function ActionButton({ action }: { action: DialogAction }) {
|
|
170
270
|
const { close } = useDialogContext();
|
|
171
271
|
const theme = useTheme();
|
|
272
|
+
const color: DialogActionColor = action.color ?? 'default';
|
|
273
|
+
const shouldCloseOnPress = action.shouldCloseOnPress ?? true;
|
|
274
|
+
|
|
275
|
+
const { background, foreground } = getActionPalette(color, theme.colors);
|
|
276
|
+
|
|
277
|
+
const handlePress = useCallback(
|
|
278
|
+
(e: GestureResponderEvent) => {
|
|
279
|
+
const onPress = action.onPress;
|
|
280
|
+
if (color === 'cancel') {
|
|
281
|
+
close(onPress ? () => onPress(e) : undefined);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (shouldCloseOnPress) {
|
|
285
|
+
close(onPress ? () => onPress(e) : undefined);
|
|
286
|
+
} else {
|
|
287
|
+
onPress?.(e);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
[action.onPress, close, color, shouldCloseOnPress],
|
|
291
|
+
);
|
|
172
292
|
|
|
173
293
|
return (
|
|
174
|
-
<
|
|
294
|
+
<TouchableOpacity
|
|
175
295
|
style={{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
296
|
+
borderRadius: 9999,
|
|
297
|
+
alignItems: 'center',
|
|
298
|
+
justifyContent: 'center',
|
|
299
|
+
backgroundColor: background,
|
|
300
|
+
opacity: action.disabled ? 0.5 : 1,
|
|
301
|
+
paddingVertical: 12,
|
|
302
|
+
paddingHorizontal: 24,
|
|
180
303
|
}}
|
|
304
|
+
onPress={handlePress}
|
|
305
|
+
disabled={action.disabled}
|
|
306
|
+
activeOpacity={0.7}
|
|
307
|
+
testID={action.testID}
|
|
181
308
|
>
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
width: 34,
|
|
187
|
-
height: 34,
|
|
188
|
-
borderRadius: 17,
|
|
189
|
-
backgroundColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)',
|
|
190
|
-
alignItems: 'center',
|
|
191
|
-
justifyContent: 'center',
|
|
192
|
-
}}
|
|
193
|
-
>
|
|
194
|
-
<Text style={{ fontSize: 18, color: theme.colors.text, lineHeight: 20 }}>
|
|
195
|
-
{'\u00D7'}
|
|
196
|
-
</Text>
|
|
197
|
-
</Pressable>
|
|
198
|
-
</View>
|
|
309
|
+
<Text style={{ fontSize: 16, fontWeight: '500', color: foreground }}>
|
|
310
|
+
{action.label}
|
|
311
|
+
</Text>
|
|
312
|
+
</TouchableOpacity>
|
|
199
313
|
);
|
|
200
314
|
}
|
|
201
315
|
|
|
316
|
+
function getActionPalette(
|
|
317
|
+
color: DialogActionColor,
|
|
318
|
+
colors: ThemeColors,
|
|
319
|
+
): { background: string; foreground: string } {
|
|
320
|
+
switch (color) {
|
|
321
|
+
case 'destructive':
|
|
322
|
+
return {
|
|
323
|
+
background: colors.negative,
|
|
324
|
+
foreground: colors.negativeForeground,
|
|
325
|
+
};
|
|
326
|
+
case 'cancel':
|
|
327
|
+
return { background: colors.contrast50, foreground: colors.text };
|
|
328
|
+
case 'default':
|
|
329
|
+
return { background: colors.primary, foreground: '#FFFFFF' };
|
|
330
|
+
/* c8 ignore next 3 -- TS exhaustiveness guard */
|
|
331
|
+
default: {
|
|
332
|
+
const _exhaustive: never = color;
|
|
333
|
+
return { background: colors.primary, foreground: '#FFFFFF' };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
202
338
|
function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
|
|
203
339
|
const style: ViewStyle[] = [
|
|
204
340
|
{
|
|
@@ -210,15 +346,44 @@ function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
|
|
|
210
346
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
211
347
|
},
|
|
212
348
|
isClosing
|
|
213
|
-
? { animation: `bloomDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle
|
|
214
|
-
: { animation: 'bloomDialogFadeIn ease-out 0.15s' } as ViewStyle,
|
|
349
|
+
? ({ animation: `bloomDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
|
|
350
|
+
: ({ animation: 'bloomDialogFadeIn ease-out 0.15s' } as ViewStyle),
|
|
215
351
|
];
|
|
216
352
|
|
|
217
353
|
return <View style={style} />;
|
|
218
354
|
}
|
|
219
355
|
|
|
220
|
-
|
|
221
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Inline imperative dialog used by `alert()`. Mounts and immediately
|
|
358
|
+
* presents; resolves the host's `onResolve` once the dialog has finished
|
|
359
|
+
* its exit animation.
|
|
360
|
+
*/
|
|
361
|
+
export function AutoMountedDialog({
|
|
362
|
+
title,
|
|
363
|
+
description,
|
|
364
|
+
actions,
|
|
365
|
+
onResolve,
|
|
366
|
+
}: {
|
|
367
|
+
title?: string;
|
|
368
|
+
description?: string;
|
|
369
|
+
actions: DialogAction[];
|
|
370
|
+
onResolve: () => void;
|
|
371
|
+
}) {
|
|
372
|
+
const control = useDialogControl();
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
control.open();
|
|
376
|
+
}, [control]);
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<Dialog
|
|
380
|
+
control={control}
|
|
381
|
+
title={title}
|
|
382
|
+
description={description}
|
|
383
|
+
actions={actions}
|
|
384
|
+
onClose={onResolve}
|
|
385
|
+
/>
|
|
386
|
+
);
|
|
222
387
|
}
|
|
223
388
|
|
|
224
389
|
/**
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal bottom-sheet shell shared by `Menu`, `Select` and `ContextMenu`.
|
|
3
|
+
*
|
|
4
|
+
* Not exported from the public `@oxyhq/bloom` surface — these three
|
|
5
|
+
* components historically used the low-level `Dialog.Outer / Inner /
|
|
6
|
+
* Handle` primitives, which are gone in 0.5.0. This shell captures the
|
|
7
|
+
* shared shape (BottomSheet + drag handle + close-on-tap context) in a
|
|
8
|
+
* single place so the three internal call sites stay symmetrical.
|
|
9
|
+
*
|
|
10
|
+
* Consumers needing the same behaviour for app code should use the public
|
|
11
|
+
* `BottomSheet` primitive directly.
|
|
12
|
+
*/
|
|
13
|
+
import React, { useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
|
14
|
+
import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
15
|
+
|
|
16
|
+
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
17
|
+
import { useTheme } from '../theme/use-theme';
|
|
18
|
+
import { Context } from './context';
|
|
19
|
+
import type { DialogControlProps } from './types';
|
|
20
|
+
|
|
21
|
+
export interface SheetShellProps {
|
|
22
|
+
control: DialogControlProps;
|
|
23
|
+
label?: string;
|
|
24
|
+
header?: React.ReactNode;
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
/** Style overrides applied to the inner padded content container. */
|
|
27
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
28
|
+
children?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bottom-sheet shell with an embedded drag handle. Exposes the bloom
|
|
33
|
+
* dialog `close()` context to descendants — `Menu.Item`, `Select.Item`
|
|
34
|
+
* etc. rely on it to dismiss the sheet after selection.
|
|
35
|
+
*/
|
|
36
|
+
export function SheetShell({
|
|
37
|
+
control,
|
|
38
|
+
label,
|
|
39
|
+
header,
|
|
40
|
+
onClose,
|
|
41
|
+
contentStyle,
|
|
42
|
+
children,
|
|
43
|
+
}: SheetShellProps) {
|
|
44
|
+
const theme = useTheme();
|
|
45
|
+
const ref = useRef<BottomSheetRef>(null);
|
|
46
|
+
const closeCallbacks = useRef<(() => void)[]>([]);
|
|
47
|
+
|
|
48
|
+
const callQueuedCallbacks = useCallback(() => {
|
|
49
|
+
const queued = closeCallbacks.current;
|
|
50
|
+
closeCallbacks.current = [];
|
|
51
|
+
for (const cb of queued) {
|
|
52
|
+
try {
|
|
53
|
+
cb();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
56
|
+
console.error('SheetShell close callback error:', e);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const open = useCallback(() => {
|
|
63
|
+
ref.current?.present();
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const close = useCallback<DialogControlProps['close']>((cb) => {
|
|
67
|
+
if (typeof cb === 'function') {
|
|
68
|
+
closeCallbacks.current.push(cb);
|
|
69
|
+
}
|
|
70
|
+
ref.current?.dismiss();
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleDismiss = useCallback(() => {
|
|
74
|
+
callQueuedCallbacks();
|
|
75
|
+
onClose?.();
|
|
76
|
+
}, [callQueuedCallbacks, onClose]);
|
|
77
|
+
|
|
78
|
+
useImperativeHandle(control.ref, () => ({ open, close }), [open, close]);
|
|
79
|
+
|
|
80
|
+
const context = useMemo(
|
|
81
|
+
() => ({ close, isWithinDialog: true }),
|
|
82
|
+
[close],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const sheetStyle = useMemo(
|
|
86
|
+
() => ({
|
|
87
|
+
maxWidth: 500,
|
|
88
|
+
backgroundColor: theme.colors.background,
|
|
89
|
+
borderRadius: 20,
|
|
90
|
+
}),
|
|
91
|
+
[theme.colors.background],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<BottomSheet
|
|
96
|
+
ref={ref}
|
|
97
|
+
onDismiss={handleDismiss}
|
|
98
|
+
enablePanDownToClose
|
|
99
|
+
detached
|
|
100
|
+
backdropOpacity={0.7}
|
|
101
|
+
style={sheetStyle}
|
|
102
|
+
>
|
|
103
|
+
<Context.Provider value={context}>
|
|
104
|
+
<SheetHandle onPress={() => close()} />
|
|
105
|
+
{header}
|
|
106
|
+
<View
|
|
107
|
+
accessibilityLabel={label}
|
|
108
|
+
style={[styles.body, { backgroundColor: theme.colors.background }, contentStyle]}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</View>
|
|
112
|
+
</Context.Provider>
|
|
113
|
+
</BottomSheet>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function SheetHandle({ onPress }: { onPress: () => void }) {
|
|
118
|
+
const theme = useTheme();
|
|
119
|
+
return (
|
|
120
|
+
<View style={styles.handleContainer}>
|
|
121
|
+
<Pressable
|
|
122
|
+
onPress={onPress}
|
|
123
|
+
accessibilityLabel="Dismiss"
|
|
124
|
+
accessibilityHint="Tap to close"
|
|
125
|
+
hitSlop={{ top: 10, bottom: 10, left: 40, right: 40 }}
|
|
126
|
+
>
|
|
127
|
+
<View style={[styles.handleBar, { backgroundColor: theme.colors.text }]} />
|
|
128
|
+
</Pressable>
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
handleContainer: {
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
width: '100%',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
zIndex: 10,
|
|
139
|
+
height: 20,
|
|
140
|
+
},
|
|
141
|
+
handleBar: {
|
|
142
|
+
top: 8,
|
|
143
|
+
width: 35,
|
|
144
|
+
height: 5,
|
|
145
|
+
borderRadius: 3,
|
|
146
|
+
alignSelf: 'center',
|
|
147
|
+
opacity: 0.5,
|
|
148
|
+
},
|
|
149
|
+
body: {
|
|
150
|
+
paddingTop: 20,
|
|
151
|
+
paddingHorizontal: 20,
|
|
152
|
+
paddingBottom: 20,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { DialogAction } from './types';
|
|
2
|
+
|
|
3
|
+
let idCounter = 0;
|
|
4
|
+
function genId(): string {
|
|
5
|
+
idCounter += 1;
|
|
6
|
+
return `bloom-alert-${idCounter}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Imperative-alert store.
|
|
11
|
+
*
|
|
12
|
+
* The store sits in module scope so `alert()` works from anywhere — event
|
|
13
|
+
* handlers, async callbacks, top-level helpers — without threading a
|
|
14
|
+
* provider context through every call site.
|
|
15
|
+
*
|
|
16
|
+
* The visible UI is owned by a single subscriber (the `<BloomDialogProvider>`
|
|
17
|
+
* mounted inside the app's React tree). When there is no subscriber yet —
|
|
18
|
+
* because `alert()` was called before the provider mounted, or because the
|
|
19
|
+
* app forgot to mount the provider — entries accumulate in the queue and
|
|
20
|
+
* drain as soon as a subscriber attaches.
|
|
21
|
+
*
|
|
22
|
+
* Multiple subscribers are not supported by design. Two providers would
|
|
23
|
+
* race for the same alert; we instead enforce a single listener and let
|
|
24
|
+
* the most recent subscription win (the older one falls back to no-op).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type AlertButtonStyle = 'default' | 'cancel' | 'destructive';
|
|
28
|
+
|
|
29
|
+
export interface AlertButton {
|
|
30
|
+
/** Button label. Required. */
|
|
31
|
+
text: string;
|
|
32
|
+
/** Tap handler. Invoked after the dialog has finished its close animation. */
|
|
33
|
+
onPress?: () => void;
|
|
34
|
+
/** Visual treatment. Defaults to `'default'`. */
|
|
35
|
+
style?: AlertButtonStyle;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AlertEntry {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
message?: string;
|
|
42
|
+
buttons: AlertButton[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Listener = (queue: AlertEntry[]) => void;
|
|
46
|
+
|
|
47
|
+
let queue: AlertEntry[] = [];
|
|
48
|
+
let listener: Listener | null = null;
|
|
49
|
+
|
|
50
|
+
function emit(): void {
|
|
51
|
+
if (listener) listener(queue);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enqueue an alert. Returns the generated id so callers can dismiss it
|
|
56
|
+
* imperatively (rare — usually the dialog dismisses itself via a button
|
|
57
|
+
* tap). The runtime guarantees this enqueues even before any provider
|
|
58
|
+
* mounts; the provider drains pending entries on subscribe.
|
|
59
|
+
*/
|
|
60
|
+
export function enqueueAlert(entry: Omit<AlertEntry, 'id'>): string {
|
|
61
|
+
const id = genId();
|
|
62
|
+
queue = [...queue, { ...entry, id }];
|
|
63
|
+
emit();
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove an alert from the queue. Called by the provider once the bloom
|
|
69
|
+
* Dialog has finished closing for that entry.
|
|
70
|
+
*/
|
|
71
|
+
export function dismissAlert(id: string): void {
|
|
72
|
+
const next = queue.filter((e) => e.id !== id);
|
|
73
|
+
if (next.length === queue.length) return;
|
|
74
|
+
queue = next;
|
|
75
|
+
emit();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Subscribe to queue changes. Replaces any previously-registered listener
|
|
80
|
+
* (single-subscriber model — see header). Returns an unsubscribe function.
|
|
81
|
+
*
|
|
82
|
+
* On subscribe, the current queue is delivered synchronously so the
|
|
83
|
+
* subscriber can render pending entries that arrived before mount.
|
|
84
|
+
*/
|
|
85
|
+
export function subscribeAlerts(fn: Listener): () => void {
|
|
86
|
+
listener = fn;
|
|
87
|
+
// Deliver the current queue immediately so a freshly-mounted provider
|
|
88
|
+
// catches up with whatever accumulated before it subscribed.
|
|
89
|
+
fn(queue);
|
|
90
|
+
return () => {
|
|
91
|
+
if (listener === fn) listener = null;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Inspect the current queue. Used internally and by tests. */
|
|
96
|
+
export function getAlertQueue(): readonly AlertEntry[] {
|
|
97
|
+
return queue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Translate an alert button to the action shape the unified `Dialog`
|
|
102
|
+
* accepts. Pure — no React, no theme — so it can be reused on web and
|
|
103
|
+
* native without forking.
|
|
104
|
+
*/
|
|
105
|
+
export function buttonToAction(button: AlertButton): DialogAction {
|
|
106
|
+
return {
|
|
107
|
+
label: button.text,
|
|
108
|
+
onPress: button.onPress ? () => button.onPress?.() : undefined,
|
|
109
|
+
color:
|
|
110
|
+
button.style === 'destructive'
|
|
111
|
+
? 'destructive'
|
|
112
|
+
: button.style === 'cancel'
|
|
113
|
+
? 'cancel'
|
|
114
|
+
: 'default',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compute the effective button set for an alert. Mirrors React Native's
|
|
120
|
+
* `Alert.alert` semantics — an empty/omitted buttons array implies a
|
|
121
|
+
* single `OK` confirmation button.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveButtons(buttons: AlertButton[] | undefined): AlertButton[] {
|
|
124
|
+
if (buttons && buttons.length > 0) return buttons;
|
|
125
|
+
return [{ text: 'OK', style: 'default' }];
|
|
126
|
+
}
|