@oxyhq/bloom 0.44.1 → 0.46.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/dialog/Dialog.js +12 -3
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +23 -8
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/index.js +29 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +73 -44
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +189 -0
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -0
- package/lib/commonjs/surfaces/api.js +83 -0
- package/lib/commonjs/surfaces/api.js.map +1 -0
- package/lib/commonjs/surfaces/index.js +82 -0
- package/lib/commonjs/surfaces/index.js.map +1 -0
- package/lib/commonjs/surfaces/index.web.js +77 -0
- package/lib/commonjs/surfaces/index.web.js.map +1 -0
- package/lib/commonjs/surfaces/prompts.js +154 -0
- package/lib/commonjs/surfaces/prompts.js.map +1 -0
- package/lib/commonjs/surfaces/surfaceStore.js +172 -0
- package/lib/commonjs/surfaces/surfaceStore.js.map +1 -0
- package/lib/commonjs/surfaces/types.js +6 -0
- package/lib/commonjs/surfaces/types.js.map +1 -0
- package/lib/commonjs/surfaces/useSurface.js +36 -0
- package/lib/commonjs/surfaces/useSurface.js.map +1 -0
- package/lib/commonjs/theme/BloomThemeProvider.js +34 -15
- package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
- package/lib/commonjs/theme/ambient-store.js +153 -0
- package/lib/commonjs/theme/ambient-store.js.map +1 -0
- package/lib/commonjs/theme/index.js +13 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/module/dialog/Dialog.js +13 -4
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +23 -8
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +5 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +184 -0
- package/lib/module/surfaces/SurfaceHost.js.map +1 -0
- package/lib/module/surfaces/api.js +48 -0
- package/lib/module/surfaces/api.js.map +1 -0
- package/lib/module/surfaces/index.js +24 -0
- package/lib/module/surfaces/index.js.map +1 -0
- package/lib/module/surfaces/index.web.js +19 -0
- package/lib/module/surfaces/index.web.js.map +1 -0
- package/lib/module/surfaces/prompts.js +148 -0
- package/lib/module/surfaces/prompts.js.map +1 -0
- package/lib/module/surfaces/surfaceStore.js +159 -0
- package/lib/module/surfaces/surfaceStore.js.map +1 -0
- package/lib/module/surfaces/types.js +4 -0
- package/lib/module/surfaces/types.js.map +1 -0
- package/lib/module/surfaces/useSurface.js +31 -0
- package/lib/module/surfaces/useSurface.js.map +1 -0
- package/lib/module/theme/BloomThemeProvider.js +34 -15
- package/lib/module/theme/BloomThemeProvider.js.map +1 -1
- package/lib/module/theme/ambient-store.js +146 -0
- package/lib/module/theme/ambient-store.js.map +1 -0
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +10 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts +25 -0
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/api.d.ts +41 -0
- package/lib/typescript/commonjs/surfaces/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/index.d.ts +25 -0
- package/lib/typescript/commonjs/surfaces/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/index.web.d.ts +20 -0
- package/lib/typescript/commonjs/surfaces/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/prompts.d.ts +13 -0
- package/lib/typescript/commonjs/surfaces/prompts.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts +51 -0
- package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/types.d.ts +145 -0
- package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/useSurface.d.ts +21 -0
- package/lib/typescript/commonjs/surfaces/useSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/ambient-store.d.ts +61 -0
- package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/index.d.ts +2 -0
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +10 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts +25 -0
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/api.d.ts +41 -0
- package/lib/typescript/module/surfaces/api.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/index.d.ts +25 -0
- package/lib/typescript/module/surfaces/index.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/index.web.d.ts +20 -0
- package/lib/typescript/module/surfaces/index.web.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/prompts.d.ts +13 -0
- package/lib/typescript/module/surfaces/prompts.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/surfaceStore.d.ts +51 -0
- package/lib/typescript/module/surfaces/surfaceStore.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/types.d.ts +145 -0
- package/lib/typescript/module/surfaces/types.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/useSurface.d.ts +21 -0
- package/lib/typescript/module/surfaces/useSurface.d.ts.map +1 -0
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/module/theme/ambient-store.d.ts +61 -0
- package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -0
- package/lib/typescript/module/theme/index.d.ts +2 -0
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/package.json +17 -1
- package/src/__tests__/ambient-theme.test.tsx +136 -0
- package/src/__tests__/surfaceStore.test.ts +219 -0
- package/src/dialog/Dialog.tsx +14 -3
- package/src/dialog/Dialog.web.tsx +25 -6
- package/src/dialog/types.ts +10 -0
- package/src/index.ts +15 -0
- package/src/index.web.ts +15 -0
- package/src/surfaces/SurfaceHost.tsx +207 -0
- package/src/surfaces/api.ts +55 -0
- package/src/surfaces/index.ts +38 -0
- package/src/surfaces/index.web.ts +33 -0
- package/src/surfaces/prompts.tsx +178 -0
- package/src/surfaces/surfaceStore.ts +168 -0
- package/src/surfaces/types.ts +156 -0
- package/src/surfaces/useSurface.ts +33 -0
- package/src/theme/BloomThemeProvider.tsx +52 -13
- package/src/theme/ambient-store.ts +186 -0
- package/src/theme/index.ts +7 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dismissAll,
|
|
3
|
+
dismissToRoot,
|
|
4
|
+
dismissTop,
|
|
5
|
+
present,
|
|
6
|
+
requestDismiss,
|
|
7
|
+
} from './surfaceStore';
|
|
8
|
+
import { confirm, prompt } from './prompts';
|
|
9
|
+
import type { PresentOptions, SurfaceRenderFn } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dismiss the TOP surface, resolving its `present()` promise with `result`.
|
|
13
|
+
* The public counterpart of the store's `dismissTop`.
|
|
14
|
+
*/
|
|
15
|
+
function dismiss(result?: unknown): void {
|
|
16
|
+
dismissTop(result);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dismiss a SPECIFIC surface by id, resolving its `present()` promise with
|
|
21
|
+
* `result`. Rarely needed — most call sites use `dismiss()` (top) or the
|
|
22
|
+
* per-surface `SurfaceControls.dismiss`.
|
|
23
|
+
*/
|
|
24
|
+
function dismissById(id: string, result?: unknown): void {
|
|
25
|
+
requestDismiss(id, result);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shared imperative surface-stack API — a module singleton (mirrors
|
|
30
|
+
* `alert()`'s shape) so any consumer, in the SDK or an app, presents into the
|
|
31
|
+
* SAME coordinated Bloom stack from anywhere without threading a provider.
|
|
32
|
+
*
|
|
33
|
+
* Exposed at the package root as `surfaces` (avoids a bare `present`/`confirm`
|
|
34
|
+
* name clash with the existing `alert-dialog` `confirm`); the individual
|
|
35
|
+
* functions are also named exports of `@oxyhq/bloom/surfaces` for direct import.
|
|
36
|
+
*/
|
|
37
|
+
export const surfaces = {
|
|
38
|
+
/** Present a new surface on top. Resolves with the dismissal result. */
|
|
39
|
+
present: <Result = unknown>(render: SurfaceRenderFn, opts?: PresentOptions) =>
|
|
40
|
+
present<Result>(render, opts),
|
|
41
|
+
/** Dismiss the top surface. */
|
|
42
|
+
dismiss,
|
|
43
|
+
/** Dismiss a specific surface by id. */
|
|
44
|
+
dismissById,
|
|
45
|
+
/** Dismiss everything above the root surface. */
|
|
46
|
+
dismissToRoot,
|
|
47
|
+
/** Dismiss the entire stack (resolves pending awaiters with `undefined`). */
|
|
48
|
+
dismissAll,
|
|
49
|
+
/** Present a confirm surface; resolves `true`/`false`. */
|
|
50
|
+
confirm,
|
|
51
|
+
/** Present a prompt surface; resolves the string or `null`. */
|
|
52
|
+
prompt,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export { confirm, dismiss, dismissAll, dismissById, dismissToRoot, present, prompt };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Dialog } from '../dialog';
|
|
2
|
+
import { createSurfaceHost } from './SurfaceHost';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The shared, content-agnostic SURFACE STACK.
|
|
6
|
+
*
|
|
7
|
+
* `<SurfaceProvider>`/`<SurfaceHost>` render the coordinated stack; the
|
|
8
|
+
* imperative `surfaces` API (+ its named functions) present into it from
|
|
9
|
+
* anywhere; `useSurface()` gives a presented surface its own controls.
|
|
10
|
+
*
|
|
11
|
+
* `SurfaceHost`/`SurfaceProvider` are bound to the NATIVE `Dialog` here; the web
|
|
12
|
+
* fork (`./index.web`) binds the web `Dialog`. The store, imperative API, hook,
|
|
13
|
+
* and types are platform-agnostic and shared by both.
|
|
14
|
+
*/
|
|
15
|
+
const { SurfaceHost, SurfaceProvider } = createSurfaceHost(Dialog);
|
|
16
|
+
|
|
17
|
+
export { SurfaceHost, SurfaceProvider };
|
|
18
|
+
export {
|
|
19
|
+
surfaces,
|
|
20
|
+
present,
|
|
21
|
+
dismiss,
|
|
22
|
+
dismissById,
|
|
23
|
+
dismissToRoot,
|
|
24
|
+
dismissAll,
|
|
25
|
+
confirm,
|
|
26
|
+
prompt,
|
|
27
|
+
} from './api';
|
|
28
|
+
export { useSurface } from './useSurface';
|
|
29
|
+
export type {
|
|
30
|
+
SurfacePresentation,
|
|
31
|
+
PresentOptions,
|
|
32
|
+
SurfaceControls,
|
|
33
|
+
SurfaceRenderFn,
|
|
34
|
+
SurfaceStatus,
|
|
35
|
+
SurfaceEntry,
|
|
36
|
+
SurfaceConfirmOptions,
|
|
37
|
+
SurfacePromptOptions,
|
|
38
|
+
} from './types';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Dialog } from '../dialog/index.web';
|
|
2
|
+
import { createSurfaceHost } from './SurfaceHost';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Web variant of the surface stack barrel. Identical surface area to the native
|
|
6
|
+
* `./index.ts` — it just binds the web-fork `Dialog` (`Dialog.web.tsx`, a
|
|
7
|
+
* pure-DOM overlay) instead of the native `BottomSheet`-backed one. The store,
|
|
8
|
+
* imperative API, hook, and types are shared, platform-agnostic modules.
|
|
9
|
+
*/
|
|
10
|
+
const { SurfaceHost, SurfaceProvider } = createSurfaceHost(Dialog);
|
|
11
|
+
|
|
12
|
+
export { SurfaceHost, SurfaceProvider };
|
|
13
|
+
export {
|
|
14
|
+
surfaces,
|
|
15
|
+
present,
|
|
16
|
+
dismiss,
|
|
17
|
+
dismissById,
|
|
18
|
+
dismissToRoot,
|
|
19
|
+
dismissAll,
|
|
20
|
+
confirm,
|
|
21
|
+
prompt,
|
|
22
|
+
} from './api';
|
|
23
|
+
export { useSurface } from './useSurface';
|
|
24
|
+
export type {
|
|
25
|
+
SurfacePresentation,
|
|
26
|
+
PresentOptions,
|
|
27
|
+
SurfaceControls,
|
|
28
|
+
SurfaceRenderFn,
|
|
29
|
+
SurfaceStatus,
|
|
30
|
+
SurfaceEntry,
|
|
31
|
+
SurfaceConfirmOptions,
|
|
32
|
+
SurfacePromptOptions,
|
|
33
|
+
} from './types';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Button } from '../button';
|
|
5
|
+
import { TextFieldInput } from '../text-field';
|
|
6
|
+
import { useTheme } from '../theme/use-theme';
|
|
7
|
+
import { present } from './surfaceStore';
|
|
8
|
+
import type {
|
|
9
|
+
SurfaceConfirmOptions,
|
|
10
|
+
SurfaceControls,
|
|
11
|
+
SurfacePromptOptions,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Built-in `confirm` / `prompt` surfaces.
|
|
16
|
+
*
|
|
17
|
+
* They are implemented AS surfaces — each calls `present(...)` so it STACKS
|
|
18
|
+
* correctly above any open surface with the right layer/z (unlike Bloom's
|
|
19
|
+
* one-at-a-time `alert()` FIFO queue, which cannot layer over another surface).
|
|
20
|
+
* Their buttons resolve through the surface's own `dismiss(result)` (NOT the
|
|
21
|
+
* underlying `Dialog`'s close), so the value flows back through the `present()`
|
|
22
|
+
* promise.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const TITLE_STYLE = { fontSize: 22, fontWeight: '600' as const, lineHeight: 30 };
|
|
26
|
+
const MESSAGE_STYLE = { fontSize: 16, lineHeight: 22 };
|
|
27
|
+
|
|
28
|
+
function ConfirmSurface({
|
|
29
|
+
options,
|
|
30
|
+
surface,
|
|
31
|
+
}: {
|
|
32
|
+
options: SurfaceConfirmOptions;
|
|
33
|
+
surface: SurfaceControls;
|
|
34
|
+
}) {
|
|
35
|
+
const theme = useTheme();
|
|
36
|
+
const onConfirm = useCallback(() => surface.dismiss(true), [surface]);
|
|
37
|
+
const onCancel = useCallback(() => surface.dismiss(false), [surface]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View>
|
|
41
|
+
<Text
|
|
42
|
+
style={[
|
|
43
|
+
TITLE_STYLE,
|
|
44
|
+
{
|
|
45
|
+
color: theme.colors.text,
|
|
46
|
+
paddingBottom: options.message ? 4 : 16,
|
|
47
|
+
},
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
{options.title}
|
|
51
|
+
</Text>
|
|
52
|
+
{options.message ? (
|
|
53
|
+
<Text
|
|
54
|
+
style={[
|
|
55
|
+
MESSAGE_STYLE,
|
|
56
|
+
{ color: theme.colors.textSecondary, paddingBottom: 16 },
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
{options.message}
|
|
60
|
+
</Text>
|
|
61
|
+
) : null}
|
|
62
|
+
<View style={{ gap: 8 }}>
|
|
63
|
+
<Button
|
|
64
|
+
variant={options.destructive ? 'destructive' : 'primary'}
|
|
65
|
+
onPress={onConfirm}
|
|
66
|
+
testID="bloom-surface-confirm-confirm"
|
|
67
|
+
>
|
|
68
|
+
{options.confirmLabel ?? 'Confirm'}
|
|
69
|
+
</Button>
|
|
70
|
+
{options.hideCancel ? null : (
|
|
71
|
+
<Button
|
|
72
|
+
variant="secondary"
|
|
73
|
+
onPress={onCancel}
|
|
74
|
+
testID="bloom-surface-confirm-cancel"
|
|
75
|
+
>
|
|
76
|
+
{options.cancelLabel ?? 'Cancel'}
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
79
|
+
</View>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function PromptSurface({
|
|
85
|
+
options,
|
|
86
|
+
surface,
|
|
87
|
+
}: {
|
|
88
|
+
options: SurfacePromptOptions;
|
|
89
|
+
surface: SurfaceControls;
|
|
90
|
+
}) {
|
|
91
|
+
const theme = useTheme();
|
|
92
|
+
const [value, setValue] = useState(options.defaultValue ?? '');
|
|
93
|
+
const onSubmit = useCallback(() => surface.dismiss(value), [surface, value]);
|
|
94
|
+
const onCancel = useCallback(() => surface.dismiss(null), [surface]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<View>
|
|
98
|
+
<Text
|
|
99
|
+
style={[
|
|
100
|
+
TITLE_STYLE,
|
|
101
|
+
{
|
|
102
|
+
color: theme.colors.text,
|
|
103
|
+
paddingBottom: options.message ? 4 : 16,
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
{options.title}
|
|
108
|
+
</Text>
|
|
109
|
+
{options.message ? (
|
|
110
|
+
<Text
|
|
111
|
+
style={[
|
|
112
|
+
MESSAGE_STYLE,
|
|
113
|
+
{ color: theme.colors.textSecondary, paddingBottom: 16 },
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
{options.message}
|
|
117
|
+
</Text>
|
|
118
|
+
) : null}
|
|
119
|
+
<TextFieldInput
|
|
120
|
+
label={options.inputLabel ?? options.title}
|
|
121
|
+
placeholder={options.placeholder}
|
|
122
|
+
value={value}
|
|
123
|
+
onChangeText={setValue}
|
|
124
|
+
autoFocus
|
|
125
|
+
returnKeyType="done"
|
|
126
|
+
onSubmitEditing={onSubmit}
|
|
127
|
+
testID="bloom-surface-prompt-input"
|
|
128
|
+
/>
|
|
129
|
+
<View style={{ gap: 8, paddingTop: 16 }}>
|
|
130
|
+
<Button
|
|
131
|
+
variant="primary"
|
|
132
|
+
onPress={onSubmit}
|
|
133
|
+
testID="bloom-surface-prompt-confirm"
|
|
134
|
+
>
|
|
135
|
+
{options.confirmLabel ?? 'OK'}
|
|
136
|
+
</Button>
|
|
137
|
+
<Button
|
|
138
|
+
variant="secondary"
|
|
139
|
+
onPress={onCancel}
|
|
140
|
+
testID="bloom-surface-prompt-cancel"
|
|
141
|
+
>
|
|
142
|
+
{options.cancelLabel ?? 'Cancel'}
|
|
143
|
+
</Button>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Present a confirm surface stacked on top of anything currently open. Resolves
|
|
151
|
+
* `true` on confirm, `false` on cancel OR on backdrop/Escape/back dismissal.
|
|
152
|
+
*/
|
|
153
|
+
export function confirm(options: SurfaceConfirmOptions): Promise<boolean> {
|
|
154
|
+
return present<unknown>(
|
|
155
|
+
(surface) => <ConfirmSurface options={options} surface={surface} />,
|
|
156
|
+
{
|
|
157
|
+
placement: options.placement ?? 'center',
|
|
158
|
+
dismissOnBackdrop: options.dismissible ?? true,
|
|
159
|
+
label: options.title,
|
|
160
|
+
},
|
|
161
|
+
).then((result) => result === true);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Present a prompt surface stacked on top of anything currently open. Resolves
|
|
166
|
+
* the entered string on submit, or `null` on cancel OR backdrop/Escape/back
|
|
167
|
+
* dismissal.
|
|
168
|
+
*/
|
|
169
|
+
export function prompt(options: SurfacePromptOptions): Promise<string | null> {
|
|
170
|
+
return present<unknown>(
|
|
171
|
+
(surface) => <PromptSurface options={options} surface={surface} />,
|
|
172
|
+
{
|
|
173
|
+
placement: options.placement ?? 'center',
|
|
174
|
+
dismissOnBackdrop: options.dismissible ?? true,
|
|
175
|
+
label: options.title,
|
|
176
|
+
},
|
|
177
|
+
).then((result) => (typeof result === 'string' ? result : null));
|
|
178
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PresentOptions,
|
|
3
|
+
SurfaceEntry,
|
|
4
|
+
SurfaceRenderFn,
|
|
5
|
+
} from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shared, content-agnostic SURFACE STACK.
|
|
9
|
+
*
|
|
10
|
+
* A module-singleton store (same pattern as `theme/ambient-store.ts` and
|
|
11
|
+
* `dialog/alert-store.ts`) holding an ordered stack of surfaces presented ON TOP
|
|
12
|
+
* of each other — bottom→top, the last entry is the top. One coordinated stack
|
|
13
|
+
* that the whole ecosystem (the SDK AND consumer apps) presents into, so
|
|
14
|
+
* overlays never clash: per-layer z, top-only interactivity, backdrop/Escape/
|
|
15
|
+
* back routed to the TOP surface, lower layers dimmed.
|
|
16
|
+
*
|
|
17
|
+
* It is CONTENT-AGNOSTIC: `present` takes a render function, not a route name.
|
|
18
|
+
* Bloom must never know about routes; the SDK layers its route registry on top.
|
|
19
|
+
*
|
|
20
|
+
* `getSnapshot` returns a STABLE reference while unchanged (the array identity
|
|
21
|
+
* only changes on a real mutation), so `useSyncExternalStore` — and the React
|
|
22
|
+
* Compiler — never see a silently-stale value.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
let idCounter = 0;
|
|
26
|
+
function genId(): string {
|
|
27
|
+
idCounter += 1;
|
|
28
|
+
return `bloom-surface-${idCounter}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A single frozen "empty stack" snapshot returned by `getSnapshot` when idle. */
|
|
32
|
+
const EMPTY: readonly SurfaceEntry[] = Object.freeze([]);
|
|
33
|
+
|
|
34
|
+
let surfaces: readonly SurfaceEntry[] = EMPTY;
|
|
35
|
+
const listeners = new Set<() => void>();
|
|
36
|
+
|
|
37
|
+
function emit(): void {
|
|
38
|
+
for (const listener of listeners) listener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Subscribe to stack changes. Returns an unsubscribe fn. */
|
|
42
|
+
export function subscribe(listener: () => void): () => void {
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
listeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Current stack snapshot (stable ref while unchanged). Bottom→top. */
|
|
50
|
+
export function getSnapshot(): readonly SurfaceEntry[] {
|
|
51
|
+
return surfaces;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Present a new surface on TOP of the stack. Returns a promise that resolves
|
|
56
|
+
* with the value passed to `dismiss(result)` (or `undefined` when the surface is
|
|
57
|
+
* dismissed without a result / by a stack-wide clear).
|
|
58
|
+
*/
|
|
59
|
+
export function present<Result = unknown>(
|
|
60
|
+
render: SurfaceRenderFn,
|
|
61
|
+
opts?: PresentOptions,
|
|
62
|
+
): Promise<Result> {
|
|
63
|
+
return new Promise<Result>((resolve) => {
|
|
64
|
+
const entry: SurfaceEntry = {
|
|
65
|
+
id: genId(),
|
|
66
|
+
render,
|
|
67
|
+
presentation: opts ?? {},
|
|
68
|
+
status: 'open',
|
|
69
|
+
resolve: resolve as (result: unknown) => void,
|
|
70
|
+
settled: false,
|
|
71
|
+
generation: 0,
|
|
72
|
+
};
|
|
73
|
+
surfaces = [...surfaces, entry];
|
|
74
|
+
emit();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Request dismissal of a surface. RESOLVES THE PROMISE IMMEDIATELY (the exit
|
|
80
|
+
* animation that follows is cosmetic) and flips the entry to `'closing'` so the
|
|
81
|
+
* host runs its exit animation. Resolution is resolve-once via the `settled`
|
|
82
|
+
* guard — a second request (or the host's post-exit `onClose`) never re-resolves
|
|
83
|
+
* with a different value. Bumps the entry `generation` so a completion callback
|
|
84
|
+
* from a superseded cycle is inert.
|
|
85
|
+
*/
|
|
86
|
+
export function requestDismiss(id: string, result?: unknown): void {
|
|
87
|
+
const entry = surfaces.find((e) => e.id === id);
|
|
88
|
+
if (!entry) return;
|
|
89
|
+
|
|
90
|
+
// Resolve-once. The mapped replacement below carries `settled: true`, so a
|
|
91
|
+
// later `requestDismiss(id)` reads the settled entry and skips this branch —
|
|
92
|
+
// no in-place mutation of the live entry is needed.
|
|
93
|
+
if (!entry.settled) {
|
|
94
|
+
entry.resolve(result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (entry.status === 'closing') return;
|
|
98
|
+
|
|
99
|
+
surfaces = surfaces.map((e) =>
|
|
100
|
+
e.id === id
|
|
101
|
+
? { ...e, settled: true, status: 'closing', generation: e.generation + 1 }
|
|
102
|
+
: e,
|
|
103
|
+
);
|
|
104
|
+
emit();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Splice a surface out of the stack once its exit animation has settled. Called
|
|
109
|
+
* by the host from the `Dialog`'s post-exit `onClose`. Idempotent and gated on
|
|
110
|
+
* `'closing'` status: a stale/duplicate call — or one for an entry that is not
|
|
111
|
+
* actually closing — is a no-op, which (together with unique entry ids) is the
|
|
112
|
+
* stale-callback hygiene `BottomSheetBase`'s generation guard provides for its
|
|
113
|
+
* reused instance.
|
|
114
|
+
*/
|
|
115
|
+
export function finalizeClose(id: string): void {
|
|
116
|
+
const entry = surfaces.find((e) => e.id === id);
|
|
117
|
+
if (!entry || entry.status !== 'closing') return;
|
|
118
|
+
const next = surfaces.filter((e) => e.id !== id);
|
|
119
|
+
surfaces = next.length === 0 ? EMPTY : next;
|
|
120
|
+
emit();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Dismiss the TOP surface, resolving its promise with `result`. */
|
|
124
|
+
export function dismissTop(result?: unknown): void {
|
|
125
|
+
const top = surfaces[surfaces.length - 1];
|
|
126
|
+
if (top) requestDismiss(top.id, result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Dismiss every surface ABOVE the bottom-most one, keeping the root surface
|
|
131
|
+
* open. Pending awaiters of the dismissed surfaces resolve with `undefined` so
|
|
132
|
+
* they never hang. No-op with 0 or 1 surfaces.
|
|
133
|
+
*/
|
|
134
|
+
export function dismissToRoot(): void {
|
|
135
|
+
if (surfaces.length <= 1) return;
|
|
136
|
+
const above = surfaces.slice(1).map((e) => e.id);
|
|
137
|
+
for (const id of above) requestDismiss(id, undefined);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Dismiss the ENTIRE stack. Every pending awaiter resolves with `undefined` so
|
|
142
|
+
* none hang.
|
|
143
|
+
*/
|
|
144
|
+
export function dismissAll(): void {
|
|
145
|
+
const ids = surfaces.map((e) => e.id);
|
|
146
|
+
for (const id of ids) requestDismiss(id, undefined);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hard-reset the stack synchronously (no exit animation), resolving all pending
|
|
151
|
+
* awaiters with `undefined`. Used when the host provider unmounts so no
|
|
152
|
+
* `present()` promise is stranded.
|
|
153
|
+
*/
|
|
154
|
+
export function resetSurfaces(): void {
|
|
155
|
+
if (surfaces.length === 0) return;
|
|
156
|
+
const pending = surfaces;
|
|
157
|
+
surfaces = EMPTY;
|
|
158
|
+
for (const entry of pending) {
|
|
159
|
+
if (!entry.settled) entry.resolve(undefined);
|
|
160
|
+
}
|
|
161
|
+
emit();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Test-only: hard-reset without notifying (drops listeners' work). */
|
|
165
|
+
export function __resetSurfacesForTests(): void {
|
|
166
|
+
surfaces = EMPTY;
|
|
167
|
+
idCounter = 0;
|
|
168
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { DialogInset, ResponsiveDialogPlacement } from '../dialog/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How a surface renders — the placement + the `Dialog` sizing/chrome options a
|
|
8
|
+
* surface needs. This is the CONTENT-AGNOSTIC presentation config: it never
|
|
9
|
+
* references a route or screen name (Bloom's stack knows nothing about routes;
|
|
10
|
+
* the SDK layers its route registry on top).
|
|
11
|
+
*
|
|
12
|
+
* The values map 1:1 onto the shared {@link ../dialog/types#DialogProps}, so a
|
|
13
|
+
* surface reads/behaves identically to a hand-mounted `<Dialog>` with the same
|
|
14
|
+
* props — only stacked/coordinated by {@link SurfaceHost}.
|
|
15
|
+
*/
|
|
16
|
+
export interface SurfacePresentation {
|
|
17
|
+
/**
|
|
18
|
+
* Surface anchor. Defaults to `{ base: 'bottom', md: 'center' }` — a bottom
|
|
19
|
+
* sheet on narrow viewports that becomes a centered modal on wide ones (the
|
|
20
|
+
* ecosystem's canonical responsive "sheet"). Pass `'center'` for a plain
|
|
21
|
+
* modal, `'left'`/`'right'` for a drawer, or any responsive placement map.
|
|
22
|
+
*/
|
|
23
|
+
placement?: ResponsiveDialogPlacement;
|
|
24
|
+
/** Side-sheet width (px) on wide screens. Forwarded to `Dialog`. */
|
|
25
|
+
width?: number;
|
|
26
|
+
/** Centered-card max width (px). Forwarded to `Dialog`. */
|
|
27
|
+
maxWidth?: number;
|
|
28
|
+
/** Bottom-sheet max height as a fraction of the viewport height. */
|
|
29
|
+
maxHeightRatio?: number;
|
|
30
|
+
/** Side-sheet inset (px) from the root overlay's edges. */
|
|
31
|
+
inset?: DialogInset;
|
|
32
|
+
/** Whether to render the drag handle in bottom-sheet mode. */
|
|
33
|
+
showHandle?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether tapping the backdrop / pressing Escape / Android-back dismisses the
|
|
36
|
+
* surface. Defaults to `true`. Set `false` for a blocking surface that must be
|
|
37
|
+
* dismissed programmatically (e.g. an unanswered required prompt).
|
|
38
|
+
*/
|
|
39
|
+
dismissOnBackdrop?: boolean;
|
|
40
|
+
/** Inner padding (px) of the surface content container. Forwarded to `Dialog`. */
|
|
41
|
+
contentPadding?: number;
|
|
42
|
+
/** Style overrides applied to the inner content container. */
|
|
43
|
+
style?: StyleProp<ViewStyle>;
|
|
44
|
+
/** Style overrides for the side/bottom placement panel surface. */
|
|
45
|
+
panelStyle?: StyleProp<ViewStyle>;
|
|
46
|
+
/** NativeWind classes for the side/bottom placement panel surface. */
|
|
47
|
+
panelClassName?: string;
|
|
48
|
+
/** Style for the root overlay (theme-var scope, rail offset, etc.). */
|
|
49
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
50
|
+
/** NativeWind classes for the root overlay. */
|
|
51
|
+
containerClassName?: string;
|
|
52
|
+
/** Accessibility label applied to the surface's dialog role. */
|
|
53
|
+
label?: string;
|
|
54
|
+
/** Stable testID forwarded to the underlying `Dialog`. */
|
|
55
|
+
testID?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options accepted by `present` / `SurfaceControls.present`. */
|
|
59
|
+
export type PresentOptions = SurfacePresentation;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Controls handed to a presented surface — both as the argument to its render
|
|
63
|
+
* function AND via {@link ../surfaces/useSurface#useSurface}. This is the DEPTH
|
|
64
|
+
* axis only: a surface can dismiss ITSELF (resolving its own `present()`
|
|
65
|
+
* promise) or `present` a CHILD surface stacked above it. Navigation WITHIN a
|
|
66
|
+
* surface (route history / drill-in) is deliberately NOT here — that is the
|
|
67
|
+
* SDK's route layer, which composes on top of this stack.
|
|
68
|
+
*/
|
|
69
|
+
export interface SurfaceControls {
|
|
70
|
+
/**
|
|
71
|
+
* Dismiss THIS surface, resolving the promise returned by the `present` call
|
|
72
|
+
* that created it with `result` (or `undefined`). Resolves immediately; the
|
|
73
|
+
* exit animation is cosmetic.
|
|
74
|
+
*/
|
|
75
|
+
dismiss: (result?: unknown) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Stack a CHILD surface directly above this one. Resolves with the child's
|
|
78
|
+
* dismissal result. The child paints above (higher layer) and captures
|
|
79
|
+
* backdrop/Escape/back until it is dismissed.
|
|
80
|
+
*/
|
|
81
|
+
present: <Result = unknown>(
|
|
82
|
+
render: SurfaceRenderFn,
|
|
83
|
+
opts?: PresentOptions,
|
|
84
|
+
) => Promise<Result>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A surface's content factory — receives its own {@link SurfaceControls}. */
|
|
88
|
+
export type SurfaceRenderFn = (surface: SurfaceControls) => ReactNode;
|
|
89
|
+
|
|
90
|
+
/** Lifecycle status of a stacked surface. */
|
|
91
|
+
export type SurfaceStatus = 'open' | 'closing';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One entry in the surface stack. `resolve` fulfils the `present()` promise; the
|
|
95
|
+
* `settled` guard makes resolution idempotent (resolve-once); `generation` is
|
|
96
|
+
* the per-entry stale-callback guard (carried from `BottomSheetBase`'s
|
|
97
|
+
* `closeGenerationRef`, keyed per entry id) so a completion callback from a
|
|
98
|
+
* superseded close cycle can never tear down the wrong state.
|
|
99
|
+
*/
|
|
100
|
+
export interface SurfaceEntry {
|
|
101
|
+
id: string;
|
|
102
|
+
render: SurfaceRenderFn;
|
|
103
|
+
presentation: SurfacePresentation;
|
|
104
|
+
status: SurfaceStatus;
|
|
105
|
+
resolve: (result: unknown) => void;
|
|
106
|
+
settled: boolean;
|
|
107
|
+
generation: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Options for the built-in {@link ../surfaces/prompts#confirm} surface. */
|
|
111
|
+
export interface SurfaceConfirmOptions {
|
|
112
|
+
/** Headline. */
|
|
113
|
+
title: string;
|
|
114
|
+
/** Supporting copy. */
|
|
115
|
+
message?: string;
|
|
116
|
+
/** Confirm button label. Defaults to `'Confirm'`. */
|
|
117
|
+
confirmLabel?: string;
|
|
118
|
+
/** Cancel button label. Defaults to `'Cancel'`. */
|
|
119
|
+
cancelLabel?: string;
|
|
120
|
+
/** Style the confirm button as destructive (negative color). */
|
|
121
|
+
destructive?: boolean;
|
|
122
|
+
/** Hide the cancel button (a single-action acknowledgement). */
|
|
123
|
+
hideCancel?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Backdrop / Escape / back dismiss (resolving `false`). Defaults to `true`;
|
|
126
|
+
* pass `false` for a blocking confirm that must be answered by a button.
|
|
127
|
+
*/
|
|
128
|
+
dismissible?: boolean;
|
|
129
|
+
/** Placement override. Defaults to `'center'`. */
|
|
130
|
+
placement?: ResponsiveDialogPlacement;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Options for the built-in {@link ../surfaces/prompts#prompt} surface. */
|
|
134
|
+
export interface SurfacePromptOptions {
|
|
135
|
+
/** Headline. */
|
|
136
|
+
title: string;
|
|
137
|
+
/** Supporting copy. */
|
|
138
|
+
message?: string;
|
|
139
|
+
/** Input placeholder. */
|
|
140
|
+
placeholder?: string;
|
|
141
|
+
/** Initial input value. */
|
|
142
|
+
defaultValue?: string;
|
|
143
|
+
/** Accessibility label for the input. Defaults to the `title`. */
|
|
144
|
+
inputLabel?: string;
|
|
145
|
+
/** Submit button label. Defaults to `'OK'`. */
|
|
146
|
+
confirmLabel?: string;
|
|
147
|
+
/** Cancel button label. Defaults to `'Cancel'`. */
|
|
148
|
+
cancelLabel?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Backdrop / Escape / back dismiss (resolving `null`). Defaults to `true`;
|
|
151
|
+
* pass `false` for a blocking prompt.
|
|
152
|
+
*/
|
|
153
|
+
dismissible?: boolean;
|
|
154
|
+
/** Placement override. Defaults to `'center'`. */
|
|
155
|
+
placement?: ResponsiveDialogPlacement;
|
|
156
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { SurfaceControls } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context carrying the CURRENT surface's controls. The host wraps each presented
|
|
7
|
+
* surface's content in a provider bound to that entry's id, so a nested surface
|
|
8
|
+
* reads its OWN controls — `dismiss` targets this surface, `present` stacks a
|
|
9
|
+
* child above it.
|
|
10
|
+
*/
|
|
11
|
+
export const SurfaceContext = createContext<SurfaceControls | null>(null);
|
|
12
|
+
SurfaceContext.displayName = 'BloomSurfaceContext';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the current surface's {@link SurfaceControls} from inside a presented
|
|
16
|
+
* surface. Equivalent to the `surface` argument the render function receives —
|
|
17
|
+
* use whichever is convenient (props vs. context).
|
|
18
|
+
*
|
|
19
|
+
* DEPTH only: `dismiss` (resolve this surface's promise + close) and `present`
|
|
20
|
+
* (stack a child). Navigation within a surface is the SDK's route layer, not
|
|
21
|
+
* this hook.
|
|
22
|
+
*
|
|
23
|
+
* @throws if called outside a presented surface.
|
|
24
|
+
*/
|
|
25
|
+
export function useSurface(): SurfaceControls {
|
|
26
|
+
const controls = useContext(SurfaceContext);
|
|
27
|
+
if (!controls) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'useSurface must be called from within a presented surface (rendered by <SurfaceProvider>/<SurfaceHost>).',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return controls;
|
|
33
|
+
}
|