@oxyhq/bloom 0.86.0 → 0.87.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/LICENSE +202 -661
- package/NOTICE +4 -1
- package/README.md +121 -449
- package/package.json +2 -2
package/NOTICE
CHANGED
package/README.md
CHANGED
|
@@ -1,515 +1,187 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<b>Bloom is the UI library every Oxy app is built with.</b><br>
|
|
3
|
+
One component set for React Native, Expo and the web, with the same props on every platform.
|
|
4
|
+
</p>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://www.npmjs.com/package/@oxyhq/bloom"><img alt="npm" src="https://img.shields.io/npm/v/@oxyhq/bloom?style=flat-square&color=440151&label=%40oxyhq%2Fbloom"></a>
|
|
8
|
+
<a href="./LICENSE"><img alt="License Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-informational?style=flat-square"></a>
|
|
9
|
+
<img alt="React Native" src="https://img.shields.io/badge/React%20Native-0.73%2B-61DAFB?style=flat-square&logo=react&logoColor=black">
|
|
10
|
+
<img alt="Expo" src="https://img.shields.io/badge/Expo-supported-000020?style=flat-square&logo=expo&logoColor=white">
|
|
11
|
+
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white">
|
|
12
|
+
<img alt="Bun" src="https://img.shields.io/badge/Bun-1.3.14-000000?style=flat-square&logo=bun&logoColor=white">
|
|
13
|
+
</p>
|
|
4
14
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
bun add @oxyhq/bloom
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
### Peer dependencies
|
|
12
|
-
|
|
13
|
-
Required:
|
|
14
|
-
|
|
15
|
-
- `react >= 18`
|
|
16
|
-
- `react-native >= 0.73`
|
|
17
|
-
- `react-native-safe-area-context >= 5`
|
|
18
|
-
|
|
19
|
-
Also required:
|
|
20
|
-
|
|
21
|
-
- `react-native-reanimated >= 3.13` (`Dialog`, `BottomSheet`, `toast`, `Loading`) — on web too: the toast engine runs on react-native-web.
|
|
22
|
-
- `react-native-gesture-handler >= 2.16.1` (`Dialog`, `BottomSheet`, toast swipe-to-dismiss) — **native** also needs the app root wrapped in `GestureHandlerRootView`, see [Dialog](#dialog); on web the toast host provides its own.
|
|
23
|
-
- `react-native-svg >= 13` (Bloom icons, Avatar `squircle` shape)
|
|
24
|
-
|
|
25
|
-
## Usage
|
|
26
|
-
|
|
27
|
-
### App root
|
|
28
|
-
|
|
29
|
-
Mount `BloomProvider` once, at the very top of the app. It composes every piece of
|
|
30
|
-
app-wide Bloom state — theme, haptics, image resolution, scroll restoration and the
|
|
31
|
-
tab-bar minimize progress — so none of them can end up at the wrong depth. It takes
|
|
32
|
-
all of `BloomThemeProvider`'s props plus `imageResolver` and `haptics`.
|
|
33
|
-
|
|
34
|
-
```tsx
|
|
35
|
-
import { BloomProvider } from '@oxyhq/bloom/provider';
|
|
36
|
-
|
|
37
|
-
<BloomProvider
|
|
38
|
-
defaultMode="system"
|
|
39
|
-
defaultColorPreset="blue"
|
|
40
|
-
persistKey="app.theme"
|
|
41
|
-
storage={storage}
|
|
42
|
-
imageResolver={(id, variant) => oxyServices.getFileDownloadUrl(id, variant)}
|
|
43
|
-
>
|
|
44
|
-
<App />
|
|
45
|
-
</BloomProvider>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Everything scrollable must be **under** it: `useScrollRestoration()` throws outside
|
|
49
|
-
its provider, so a list rendered beside the root (a right rail, an overlay) crashes
|
|
50
|
-
the screen.
|
|
51
|
-
|
|
52
|
-
Outlets are **not** included — their position in the tree is a real app decision, and
|
|
53
|
-
a second mount duplicates every surface they render. Mount these yourself, under
|
|
54
|
-
`BloomProvider`: `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`,
|
|
55
|
-
`<SurfaceHost>`, `<BloomDialogProvider>`, `<AlertDialogHost>`.
|
|
56
|
-
|
|
57
|
-
### Theme
|
|
58
|
-
|
|
59
|
-
`BloomProvider` already mounts `BloomThemeProvider`; mount it directly only when you
|
|
60
|
-
need a nested/scoped theme. It accepts controlled `mode` and `colorPreset` props — persist them however you like (AsyncStorage, Zustand, etc.).
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
import { BloomThemeProvider } from '@oxyhq/bloom/theme';
|
|
64
|
-
|
|
65
|
-
<BloomThemeProvider mode="system" colorPreset="teal">
|
|
66
|
-
<App />
|
|
67
|
-
</BloomThemeProvider>
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Access theme values in any component:
|
|
71
|
-
|
|
72
|
-
```tsx
|
|
73
|
-
import { useTheme } from '@oxyhq/bloom/theme';
|
|
74
|
-
|
|
75
|
-
const theme = useTheme();
|
|
76
|
-
// theme.colors.primary, theme.colors.text, theme.isDark, etc.
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
10 color presets: `teal`, `blue`, `green`, `amber`, `red`, `purple`, `pink`, `sky`, `orange`, `mint`.
|
|
80
|
-
|
|
81
|
-
4 modes: `light`, `dark`, `system`, `adaptive` (uses iOS/Android native dynamic colors when available).
|
|
82
|
-
|
|
83
|
-
### Overlay components: Dialog, BottomSheet, toast / alert
|
|
84
|
-
|
|
85
|
-
Bloom ships **two** overlay surface components plus feedback helpers:
|
|
86
|
-
|
|
87
|
-
| Component | Use when |
|
|
88
|
-
|-----------|----------|
|
|
89
|
-
| `Dialog` | All modal surfaces — centered card, side-sheet (left/right), or bottom-sheet — via the `placement` prop. Confirmation flows AND custom content. |
|
|
90
|
-
| `BottomSheet` | You need direct control over snap points, scroll handoff, gesture coordination, or detached presentation. |
|
|
91
|
-
| `toast` / `alert` | Passive feedback (`toast`) or one-shot confirmations (`alert`) called imperatively from anywhere. |
|
|
92
|
-
|
|
93
|
-
> **Removed in 0.16.x:** `CenteredDialog` and `ResponsiveSheet` no longer exist. Migrate: `<CenteredDialog visible={v} onClose={c}>…</CenteredDialog>` → `<Dialog placement="center" open={v} onClose={c}>…</Dialog>`; `<ResponsiveSheet side="left" open={o} onClose={c}>…</ResponsiveSheet>` → `<Dialog placement={{ base:'bottom', md:'left' }} open={o} onClose={c}>…</Dialog>`.
|
|
94
|
-
|
|
95
|
-
### Dialog
|
|
96
|
-
|
|
97
|
-
A single `<Dialog>` component for every overlay surface — centered modal, side-sheet, or bottom-sheet — selected by the `placement` prop. Same component and same props on every platform.
|
|
98
|
-
|
|
99
|
-
> **Required providers (native).** Your app root **must** be wrapped with `GestureHandlerRootView` from `react-native-gesture-handler` for the bottom-sheet pan gestures to work.
|
|
100
|
-
|
|
101
|
-
```tsx
|
|
102
|
-
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
103
|
-
import { BloomThemeProvider, BloomDialogProvider } from '@oxyhq/bloom';
|
|
104
|
-
|
|
105
|
-
export default function Root() {
|
|
106
|
-
return (
|
|
107
|
-
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
108
|
-
<BloomThemeProvider mode="system" colorPreset="oxy">
|
|
109
|
-
<BloomDialogProvider>
|
|
110
|
-
<App />
|
|
111
|
-
</BloomDialogProvider>
|
|
112
|
-
</BloomThemeProvider>
|
|
113
|
-
</GestureHandlerRootView>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`BloomDialogProvider` powers the imperative `alert()` helper — mount it once near the app root.
|
|
119
|
-
|
|
120
|
-
#### Declarative (the 90% case)
|
|
121
|
-
|
|
122
|
-
```tsx
|
|
123
|
-
import { Dialog, useDialogControl } from '@oxyhq/bloom';
|
|
124
|
-
|
|
125
|
-
function SignOutButton() {
|
|
126
|
-
const control = useDialogControl();
|
|
127
|
-
return (
|
|
128
|
-
<>
|
|
129
|
-
<Button onPress={() => control.open()}>Sign out</Button>
|
|
130
|
-
|
|
131
|
-
<Dialog
|
|
132
|
-
control={control}
|
|
133
|
-
title="Sign out?"
|
|
134
|
-
description="You'll need to enter your password to sign in again."
|
|
135
|
-
actions={[
|
|
136
|
-
{ label: 'Sign out', color: 'destructive', onPress: doSignOut },
|
|
137
|
-
{ label: 'Cancel', color: 'cancel' },
|
|
138
|
-
]}
|
|
139
|
-
/>
|
|
140
|
-
</>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
#### Controlled open state
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
<Dialog
|
|
149
|
-
placement="center"
|
|
150
|
-
open={isOpen}
|
|
151
|
-
onClose={() => setIsOpen(false)}
|
|
152
|
-
title="Confirm?"
|
|
153
|
-
actions={[{ label: 'OK', onPress: handleOk }, { label: 'Cancel', color: 'cancel' }]}
|
|
154
|
-
/>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
#### Side-sheet (drawer)
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
// Fixed left side-sheet
|
|
161
|
-
<Dialog placement="left" control={control} title="Filters">
|
|
162
|
-
<FilterPanel />
|
|
163
|
-
</Dialog>
|
|
164
|
-
|
|
165
|
-
// Responsive: bottom-sheet on mobile, left drawer on desktop
|
|
166
|
-
<Dialog placement={{ base: 'bottom', md: 'left' }} control={control} title="Filters">
|
|
167
|
-
<FilterPanel />
|
|
168
|
-
</Dialog>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### Custom content
|
|
172
|
-
|
|
173
|
-
Provide any JSX as `children`. Combine with `title` to keep a consistent header. Set `contentPadding={0}` when the children own their own insets.
|
|
15
|
+
---
|
|
174
16
|
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
</Dialog>
|
|
179
|
-
```
|
|
17
|
+
<table>
|
|
18
|
+
<tr>
|
|
19
|
+
<td valign="top" width="50%">
|
|
180
20
|
|
|
181
|
-
|
|
21
|
+
### What it is
|
|
182
22
|
|
|
183
|
-
|
|
23
|
+
Components, hooks and design tokens published as `@oxyhq/bloom` across 88 subpath exports, shipped as `src` for Metro and as compiled CommonJS and ESM for everyone else. Web builds resolve platform forks automatically through export conditions, so a `.web.tsx` fork never reaches a native bundle.
|
|
184
24
|
|
|
185
|
-
|
|
186
|
-
<Dialog control={control}>
|
|
187
|
-
<YourEntirelyCustomLayout />
|
|
188
|
-
</Dialog>
|
|
189
|
-
```
|
|
25
|
+
Styling is NativeWind classes throughout. There are no colour props and no wrapper components to theme a button, because a second way to set a colour is a second thing that can disagree.
|
|
190
26
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `control?` — from `useDialogControl()`. Omit when using controlled `open` instead.
|
|
194
|
-
- `open?` — controlled open state. When provided, wins over `control`.
|
|
195
|
-
- `placement?` — `'center' | 'left' | 'right' | 'bottom'` or a responsive map `{ base; sm?; md?; lg?; xl? }`. Default: `'center'`. Breakpoints: sm 640 / md 768 / lg 1024 / xl 1280 px.
|
|
196
|
-
- `title?: string` — header text.
|
|
197
|
-
- `description?: string` — supporting copy rendered below the title.
|
|
198
|
-
- `actions?: DialogAction[]` — confirmation buttons. Each action:
|
|
199
|
-
- `label: string`
|
|
200
|
-
- `color?: 'default' | 'cancel' | 'destructive'` — defaults to `'default'`.
|
|
201
|
-
- `onPress?: (e) => void` — invoked after the dialog finishes closing.
|
|
202
|
-
- `disabled?: boolean`
|
|
203
|
-
- `shouldCloseOnPress?: boolean` — defaults to `true`. Set `false` while an async action is in flight.
|
|
204
|
-
- `testID?: string`
|
|
205
|
-
- `children?: React.ReactNode` — custom content rendered after the description.
|
|
206
|
-
- `onClose?: () => void` — fires after the dialog has finished closing. In controlled mode, the host must flip `open` to `false`.
|
|
207
|
-
- `contentPadding?: number` — inner padding of the dialog body. Default `20`. Set `0` for custom children that own their own insets.
|
|
208
|
-
- `width?: number` — side-sheet width (px). Default `460`.
|
|
209
|
-
- `maxWidth?: number` — centered-card max width (px). Default `480`.
|
|
210
|
-
- `maxHeightRatio?: number` — bottom-sheet height as a fraction of viewport height. Default `0.9`.
|
|
211
|
-
- `inset?: { top?; bottom?; left?; right? }` — side-sheet inset (px) from the overlay container edges.
|
|
212
|
-
- `showHandle?: boolean` — show drag handle in bottom-sheet mode. Default `true`.
|
|
213
|
-
- `dismissOnBackdrop?: boolean` — tap backdrop to dismiss. Default `true`.
|
|
214
|
-
- `panelStyle? / panelClassName?` — style/class for the panel surface.
|
|
215
|
-
- `containerStyle? / containerClassName?` — style/class for the root overlay (e.g. rail offset, theme-var scope).
|
|
216
|
-
- `label?: string` — accessibility label.
|
|
217
|
-
- `testID?: string`
|
|
218
|
-
|
|
219
|
-
#### Web setup
|
|
220
|
-
|
|
221
|
-
Inject the CSS animations into your global styles once:
|
|
27
|
+
</td>
|
|
28
|
+
<td valign="top" width="50%">
|
|
222
29
|
|
|
223
|
-
|
|
224
|
-
import { BLOOM_DIALOG_CSS } from '@oxyhq/bloom/dialog';
|
|
30
|
+
### How it fits the Oxy platform
|
|
225
31
|
|
|
226
|
-
|
|
227
|
-
<style>{BLOOM_DIALOG_CSS}</style>
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### toast
|
|
32
|
+
Bloom is the presentation layer of [**oxy**](https://github.com/OxyHQ/oxy). It knows nothing about identity or the network, so it has no dependency on the Oxy SDK and can be used on its own.
|
|
231
33
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
```tsx
|
|
235
|
-
import { toast } from '@oxyhq/bloom';
|
|
236
|
-
import { ToastOutlet } from '@oxyhq/bloom/toast';
|
|
34
|
+
Where an app does use the SDK, the two meet at one seam: register `imageResolver` once at the root, usually `oxyServices.getFileDownloadUrl` from `@oxyhq/core`, and every `Avatar` and image in the tree resolves bare file ids for free.
|
|
237
35
|
|
|
238
|
-
|
|
239
|
-
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
</table>
|
|
240
39
|
|
|
241
|
-
|
|
242
|
-
toast('Saved');
|
|
243
|
-
toast.success('Profile updated');
|
|
244
|
-
toast.error('Network error', { duration: 5000 });
|
|
245
|
-
toast.warning('Please verify your email');
|
|
246
|
-
toast.info('A new version is available');
|
|
40
|
+
## Install
|
|
247
41
|
|
|
248
|
-
|
|
249
|
-
|
|
42
|
+
```bash
|
|
43
|
+
bun add @oxyhq/bloom
|
|
250
44
|
```
|
|
251
45
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'warning' | 'info' | 'loading'`) is overridden by the typed helpers above; the
|
|
255
|
-
full surface (`promise`, `custom`, `wiggle`, `action`/`description`, stacking,
|
|
256
|
-
positions) is documented in [docs/toast.mdx](docs/toast.mdx).
|
|
46
|
+
<details>
|
|
47
|
+
<summary><b>Peer dependencies</b></summary>
|
|
257
48
|
|
|
258
|
-
|
|
49
|
+
<br>
|
|
259
50
|
|
|
260
|
-
|
|
51
|
+
Always required:
|
|
261
52
|
|
|
262
|
-
|
|
263
|
-
|
|
53
|
+
| Package | Range |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `react` | `>=18.0.0` |
|
|
56
|
+
| `react-dom` | `>=18.0.0` |
|
|
57
|
+
| `react-native` | `>=0.73.0` |
|
|
58
|
+
| `react-native-safe-area-context` | `>=5.0.0` |
|
|
59
|
+
| `react-native-screens` | `>=3.16.0` |
|
|
60
|
+
| `nativewind` | `>=5.0.0` |
|
|
264
61
|
|
|
265
|
-
|
|
266
|
-
{ text: 'Cancel', style: 'cancel' },
|
|
267
|
-
{ text: 'Sign out', style: 'destructive', onPress: doSignOut },
|
|
268
|
-
]);
|
|
62
|
+
Required by specific surfaces, and by more of them than you would guess:
|
|
269
63
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
64
|
+
| Package | Range | Needed by |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `react-native-reanimated` | `>=3.13.0` | `Dialog`, `BottomSheet`, `toast`, `Loading`, on the web too |
|
|
67
|
+
| `react-native-gesture-handler` | `>=2.16.1` | `Dialog`, `BottomSheet`, swipe to dismiss on toasts |
|
|
68
|
+
| `react-native-svg` | `>=13.0.0` | icons, `Avatar` in `squircle` shape |
|
|
69
|
+
| `react-native-keyboard-controller` | `>=1.11.4` | keyboard aware surfaces |
|
|
70
|
+
| `expo`, `expo-blur`, `expo-font`, `expo-haptics`, `expo-image`, `expo-symbols` | see `package.json` | native effects, fonts, haptics, images and SF Symbols |
|
|
71
|
+
| `expo-glass-effect` | `>=0.1.9` | glass surfaces |
|
|
72
|
+
| `expo-router` | `>=3.0.0` | the `expo-router` variants of `tabs`, `scroll` and `tab-bar` |
|
|
73
|
+
| `@react-native-community/netinfo` | `>=11.1.0` | `connection-status` |
|
|
273
74
|
|
|
274
|
-
|
|
75
|
+
On native, wrap the app root in `GestureHandlerRootView` from `react-native-gesture-handler`, or the bottom sheet pan gestures will not fire.
|
|
275
76
|
|
|
276
|
-
|
|
277
|
-
- `style?: 'default' | 'cancel' | 'destructive'` — defaults to `'default'`.
|
|
278
|
-
- `onPress?: () => void` — fires after the dialog finishes closing.
|
|
77
|
+
</details>
|
|
279
78
|
|
|
280
|
-
|
|
79
|
+
## Getting started
|
|
281
80
|
|
|
282
|
-
|
|
81
|
+
Mount `BloomProvider` once, at the very top of the app. It composes every piece of app wide Bloom state, theme, haptics, image resolution, scroll restoration and tab bar minimise progress, so none of them can end up at the wrong depth.
|
|
283
82
|
|
|
284
83
|
```tsx
|
|
285
|
-
import {
|
|
286
|
-
import { BottomSheet, type BottomSheetRef } from '@oxyhq/bloom/bottom-sheet';
|
|
287
|
-
|
|
288
|
-
function Example() {
|
|
289
|
-
const sheetRef = useRef<BottomSheetRef>(null);
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<>
|
|
293
|
-
<Button onPress={() => sheetRef.current?.present()}>Open</Button>
|
|
294
|
-
|
|
295
|
-
<BottomSheet ref={sheetRef} onDismiss={() => console.log('dismissed')}>
|
|
296
|
-
<Text>Sheet content</Text>
|
|
297
|
-
</BottomSheet>
|
|
298
|
-
</>
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
`BottomSheetRef` methods: `present()`, `dismiss()`, `close()`, `expand()`, `collapse()`, `scrollTo(y, animated?)`.
|
|
304
|
-
|
|
305
|
-
`BottomSheetProps`:
|
|
306
|
-
|
|
307
|
-
- `children`
|
|
308
|
-
- `onDismiss?: () => void`
|
|
309
|
-
- `enablePanDownToClose?: boolean` — defaults to `true`.
|
|
310
|
-
- `enableHandlePanningGesture?: boolean` — defaults to `true`.
|
|
311
|
-
- `onDismissAttempt?: () => boolean` — return `false` to veto a dismiss attempt.
|
|
312
|
-
- `detached?: boolean` — when `true`, the sheet floats with horizontal margins and rounded corners on all sides; when `false`, it's flush to the bottom edges with rounded top corners only.
|
|
313
|
-
- `showHandle?: boolean` — defaults to `true`. Toggles the drag handle pill at the top of the sheet.
|
|
314
|
-
- `backdropOpacity?: number` — opacity (0–1) of the dimming backdrop once fully visible. Defaults to `0.5`. Use a higher value (e.g. `0.7`) when stacking a sheet over another sheet.
|
|
315
|
-
- `backgroundComponent?` — custom background renderer.
|
|
316
|
-
- `backdropComponent?` — custom backdrop renderer.
|
|
317
|
-
- `style?`
|
|
318
|
-
- `scrollable?: boolean` — defaults to `true`. When `false`, renders `children` directly without the internal `Animated.ScrollView` wrapper. **Required when the sheet's content owns its own scrolling primitive** (e.g. `FlatList`, `SectionList`, or any `VirtualizedList`) — nesting a virtualized list inside the internal ScrollView breaks windowing and triggers a React Native warning. Combine with `manualActivation` so the handle stays draggable while the inner list owns the scroll.
|
|
319
|
-
- `manualActivation?: boolean` — defaults to `false`. When `true`, the body pan uses RNGH's `manualActivation` and only activates when (a) the inner ScrollView is at the top AND (b) the user has moved their finger downward by > 8dp. This is the `@gorhom/bottom-sheet` coordination model — recommended for sheets that contain a scrolling region on Android (the legacy always-active pan can steal vertical events from the inner scroller). Enabling this also gives the drag handle its own dedicated, unconditionally-active gesture so users can always grab the handle even mid-scroll.
|
|
320
|
-
- `dynamicBackdrop?: boolean` — defaults to `false`. When `true`, the backdrop dims proportionally to drag distance — fades from full `backdropOpacity` (sheet at rest) to 30% as the sheet is pulled down 40% of the screen height. This is the iOS Photos / iMessage drag-to-dismiss look. The base `backdropOpacity` still controls the resting dim.
|
|
321
|
-
- `handleComponent?: () => React.ReactNode` — custom drag-handle renderer. When provided (and `showHandle` is `true`), replaces the default 36×5 pill. In `manualActivation` mode the rendered handle sits inside the dedicated handle hit-area and gesture detector so it remains unconditionally draggable.
|
|
322
|
-
|
|
323
|
-
**Pattern: sheet with a `FlatList` inside.** Use `scrollable={false}` so the BottomSheet doesn't wrap the list in its own ScrollView, plus `manualActivation` so the drag handle remains the dedicated drag-to-dismiss surface while the list owns vertical scroll:
|
|
324
|
-
|
|
325
|
-
```tsx
|
|
326
|
-
<BottomSheet ref={sheetRef} scrollable={false} manualActivation>
|
|
327
|
-
<FlatList data={items} renderItem={renderItem} />
|
|
328
|
-
</BottomSheet>
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
**Pattern: iOS Photos-style backdrop dim.** Combine `manualActivation` (so the inner photo grid keeps scroll ownership) with `dynamicBackdrop` (so the overlay fades as the user pulls down):
|
|
84
|
+
import { BloomProvider } from '@oxyhq/bloom/provider';
|
|
332
85
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
backdropOpacity={0.85}
|
|
86
|
+
<BloomProvider
|
|
87
|
+
defaultMode="system"
|
|
88
|
+
defaultColorPreset="blue"
|
|
89
|
+
persistKey="app.theme"
|
|
90
|
+
storage={storage}
|
|
91
|
+
imageResolver={(id, variant) => oxyServices.getFileDownloadUrl(id, variant)}
|
|
340
92
|
>
|
|
341
|
-
<
|
|
342
|
-
</
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Button
|
|
346
|
-
|
|
347
|
-
```tsx
|
|
348
|
-
import { Button, PrimaryButton, SecondaryButton, IconButton, GhostButton, TextButton } from '@oxyhq/bloom/button';
|
|
349
|
-
|
|
350
|
-
<Button variant="primary" size="large" onPress={handlePress}>
|
|
351
|
-
Save
|
|
352
|
-
</Button>
|
|
353
|
-
|
|
354
|
-
<IconButton icon={<TrashIcon />} onPress={handleDelete} />
|
|
355
|
-
|
|
356
|
-
<SecondaryButton disabled>Cancel</SecondaryButton>
|
|
93
|
+
<App />
|
|
94
|
+
</BloomProvider>
|
|
357
95
|
```
|
|
358
96
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
### GroupedButtons
|
|
97
|
+
Everything scrollable must sit under it. `useScrollRestoration()` throws outside its provider, so a list rendered beside the root, a right rail or an overlay, crashes that screen.
|
|
362
98
|
|
|
363
|
-
|
|
99
|
+
Outlets are deliberately not included, because where they sit in the tree is a real application decision and a second mount duplicates every surface they render. Mount these yourself, under `BloomProvider`: `ToastOutlet`, the `Provider` and `Outlet` pair from `@oxyhq/bloom/portal`, `SurfaceHost`, `BloomDialogProvider` and `AlertDialogHost`.
|
|
364
100
|
|
|
365
101
|
```tsx
|
|
366
|
-
import {
|
|
102
|
+
import { Button } from '@oxyhq/bloom/button';
|
|
103
|
+
import { toast } from '@oxyhq/bloom/toast';
|
|
367
104
|
|
|
368
|
-
<
|
|
369
|
-
<GroupedButtons.Item label="Edit Profile" icon={<EditIcon />} onPress={handleEdit} />
|
|
370
|
-
<GroupedButtons.Item label="Settings" onPress={handleSettings} />
|
|
371
|
-
<GroupedButtons.Item label="Delete Account" destructive onPress={handleDelete} />
|
|
372
|
-
</GroupedButtons>
|
|
105
|
+
<Button onPress={() => toast.success('Saved')}>Save</Button>
|
|
373
106
|
```
|
|
374
107
|
|
|
375
|
-
|
|
108
|
+
## Theming
|
|
376
109
|
|
|
377
|
-
|
|
378
|
-
|
|
110
|
+
<table>
|
|
111
|
+
<tr>
|
|
112
|
+
<td valign="top" width="50%">
|
|
379
113
|
|
|
380
|
-
|
|
381
|
-
<Divider spacing={16} color="#ccc" />
|
|
382
|
-
<Divider vertical />
|
|
383
|
-
```
|
|
114
|
+
**Four modes**
|
|
384
115
|
|
|
385
|
-
|
|
116
|
+
`light`, `dark`, `system`, and `adaptive`, which follows the iOS and Android dynamic colours when the platform offers them.
|
|
386
117
|
|
|
387
118
|
```tsx
|
|
388
|
-
import {
|
|
389
|
-
|
|
390
|
-
<RadioIndicator selected={isSelected} />
|
|
391
|
-
<RadioIndicator selected={true} size={24} selectedColor="#007AFF" />
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### Avatar
|
|
395
|
-
|
|
396
|
-
Supports circle and squircle shapes. Squircle requires `react-native-svg`.
|
|
397
|
-
|
|
398
|
-
```tsx
|
|
399
|
-
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
119
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
400
120
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<Avatar fallbackSource={require('./default.png')} />
|
|
121
|
+
const theme = useTheme();
|
|
122
|
+
// theme.colors.primary, theme.colors.text, theme.isDark
|
|
404
123
|
```
|
|
405
124
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
4 variants: `spinner`, `top` (animated collapse/expand), `skeleton`, `inline`.
|
|
409
|
-
|
|
410
|
-
```tsx
|
|
411
|
-
import { Loading } from '@oxyhq/bloom/loading';
|
|
125
|
+
`useTheme()` throws outside a provider, so hoist `BloomProvider` above the splash and loading branches, not just above the main tree.
|
|
412
126
|
|
|
413
|
-
|
|
414
|
-
<
|
|
415
|
-
<Loading variant="top" showLoading={isRefreshing} />
|
|
416
|
-
<Loading variant="skeleton" lines={4} />
|
|
417
|
-
<Loading variant="inline" text="Saving..." />
|
|
418
|
-
```
|
|
127
|
+
</td>
|
|
128
|
+
<td valign="top" width="50%">
|
|
419
129
|
|
|
420
|
-
|
|
130
|
+
**Eighteen colour presets**
|
|
421
131
|
|
|
422
|
-
|
|
132
|
+
`teal`, `blue`, `green`, `yellow`, `red`, `purple`, `pink`, `sky`, `orange`, `mint`, `pumpkin`, `gray`, `brown`, `peach`, `rose`, plus `oxy` and `faircoin`, which are reserved for the accounts whose brands they are, and `mono`, which ships with a subscription.
|
|
423
133
|
|
|
424
|
-
|
|
425
|
-
import { Collapsible } from '@oxyhq/bloom/collapsible';
|
|
134
|
+
Every palette is generated from a single seed colour by a dependency free colour engine, into the full Material 3 role set for light and dark. A colour a user picks themselves runs through exactly the same path, so a preset is only a fixed seed.
|
|
426
135
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
</
|
|
430
|
-
```
|
|
136
|
+
</td>
|
|
137
|
+
</tr>
|
|
138
|
+
</table>
|
|
431
139
|
|
|
432
|
-
|
|
140
|
+
## Components
|
|
433
141
|
|
|
434
|
-
|
|
435
|
-
import { ErrorBoundary } from '@oxyhq/bloom/error-boundary';
|
|
142
|
+
Bloom publishes 88 subpath exports. Importing from the subpath rather than the root keeps a bundle to what it actually renders.
|
|
436
143
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
144
|
+
| Group | Exports |
|
|
145
|
+
|---|---|
|
|
146
|
+
| Providers and theme | `provider`, `theme`, `color-presets`, `preset-vars`, `design-tokens`, `tailwind-preset`, `styles`, `hooks` |
|
|
147
|
+
| Overlays | `dialog`, `alert-dialog`, `bottom-sheet`, `popover`, `context-menu`, `menu`, `tooltip`, `overlay`, `portal`, `surfaces` |
|
|
148
|
+
| Actions | `button`, `fab`, `frosted-icon-button`, `grouped-buttons`, `pressable-scale`, `pressable-with-hover`, `subtle-hover` |
|
|
149
|
+
| Forms | `text-field`, `field`, `input-group`, `label`, `select`, `combobox`, `command`, `checkbox`, `switch`, `slider`, `segmented-control`, `search`, `prompt-input` |
|
|
150
|
+
| Layout and lists | `grid`, `list`, `scroll`, `tabs`, `tab-bar`, `settings-list`, `content-panel`, `card`, `accordion`, `collapsible`, `divider`, `item` |
|
|
151
|
+
| Identity and media | `avatar`, `avatar-group`, `user-hover-card`, `profile-card`, `image-resolver`, `image-aspect-ratio-cache`, `zoomable-image-gallery`, `media-inset-border`, `progressive-blur`, `fill` |
|
|
152
|
+
| Feedback and data | `toast`, `admonition`, `loading`, `skeleton`, `error-boundary`, `badge`, `chip`, `kbd`, `code`, `link-preview`, `connection-status`, `connection-dots` |
|
|
153
|
+
| Charts and motion | `composition-bar`, `dot-grid-meter`, `stat-bar`, `activity-heatmap`, `motion`, `animated-check`, `icon-circle`, `radio-indicator` |
|
|
154
|
+
| Assets | `icons`, `typography`, `fonts`, `benefit-list` |
|
|
440
155
|
|
|
441
|
-
|
|
442
|
-
title="Oops!"
|
|
443
|
-
message="Something broke."
|
|
444
|
-
retryLabel="Retry"
|
|
445
|
-
fallback={<CustomFallback />}
|
|
446
|
-
>
|
|
447
|
-
<RiskyComponent />
|
|
448
|
-
</ErrorBoundary>
|
|
449
|
-
```
|
|
156
|
+
`tabs`, `scroll` and `tab-bar` each ship an `/expo-router` variant for apps on Expo Router.
|
|
450
157
|
|
|
451
|
-
|
|
158
|
+
## Documentation
|
|
452
159
|
|
|
453
|
-
|
|
160
|
+
Component guides live in [`docs/`](./docs): [getting started](./docs/getting-started.mdx), [theme](./docs/theme.mdx), [design tokens](./docs/design-tokens.mdx), [dialog](./docs/dialog.mdx), [bottom sheet](./docs/bottom-sheet.mdx), [toast](./docs/toast.mdx), [alert](./docs/alert.mdx), [menu](./docs/menu.mdx), [select](./docs/select.mdx), [context menu](./docs/context-menu.mdx), [button](./docs/button.mdx), [avatar](./docs/avatar.mdx), [text field](./docs/text-field.mdx), [settings list](./docs/settings-list.mdx), [tab bar](./docs/tab-bar.mdx), [prompt input](./docs/prompt-input.mdx) and [loading](./docs/loading.mdx).
|
|
454
161
|
|
|
455
|
-
|
|
456
|
-
import {
|
|
457
|
-
PromptInput,
|
|
458
|
-
PromptInputTextarea,
|
|
459
|
-
PromptInputActions,
|
|
460
|
-
PromptInputAttachments,
|
|
461
|
-
PromptInputSubmitButton,
|
|
462
|
-
} from '@oxyhq/bloom/prompt-input';
|
|
463
|
-
|
|
464
|
-
// Simple mode — renders built-in layout
|
|
465
|
-
<PromptInput
|
|
466
|
-
value={text}
|
|
467
|
-
onValueChange={setText}
|
|
468
|
-
onSubmit={handleSend}
|
|
469
|
-
isLoading={isGenerating}
|
|
470
|
-
onStop={handleStop}
|
|
471
|
-
placeholder="Ask anything..."
|
|
472
|
-
/>
|
|
473
|
-
|
|
474
|
-
// Compound mode — full control over layout
|
|
475
|
-
<PromptInput value={text} onValueChange={setText} onSubmit={handleSend}>
|
|
476
|
-
<PromptInputAttachments />
|
|
477
|
-
<PromptInputTextarea placeholder="Type a message..." />
|
|
478
|
-
<PromptInputActions>
|
|
479
|
-
<MyAddButton />
|
|
480
|
-
<PromptInputSubmitButton isLoading={isGenerating} onStop={handleStop} />
|
|
481
|
-
</PromptInputActions>
|
|
482
|
-
</PromptInput>
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
## Sub-path exports
|
|
486
|
-
|
|
487
|
-
```ts
|
|
488
|
-
import { BloomThemeProvider, useTheme } from '@oxyhq/bloom/theme';
|
|
489
|
-
import { Dialog, BloomDialogProvider, alert, useDialogControl } from '@oxyhq/bloom/dialog';
|
|
490
|
-
import { BottomSheet, type BottomSheetRef } from '@oxyhq/bloom/bottom-sheet';
|
|
491
|
-
import { toast, ToastOutlet } from '@oxyhq/bloom/toast';
|
|
492
|
-
import { Button, IconButton } from '@oxyhq/bloom/button';
|
|
493
|
-
import { GroupedButtons } from '@oxyhq/bloom/grouped-buttons';
|
|
494
|
-
import { Divider } from '@oxyhq/bloom/divider';
|
|
495
|
-
import { RadioIndicator } from '@oxyhq/bloom/radio-indicator';
|
|
496
|
-
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
497
|
-
import { Loading } from '@oxyhq/bloom/loading';
|
|
498
|
-
import { Collapsible } from '@oxyhq/bloom/collapsible';
|
|
499
|
-
import { ErrorBoundary } from '@oxyhq/bloom/error-boundary';
|
|
500
|
-
import { PromptInput, PromptInputTextarea } from '@oxyhq/bloom/prompt-input';
|
|
501
|
-
```
|
|
162
|
+
Upgrade notes are in [MIGRATION.md](./MIGRATION.md), and the theme reference is in [README.theme.md](./README.theme.md).
|
|
502
163
|
|
|
503
164
|
## Development
|
|
504
165
|
|
|
505
|
-
```
|
|
166
|
+
```bash
|
|
506
167
|
bun install
|
|
507
|
-
bun run build # react-native-builder-bob
|
|
508
|
-
bun run typescript # type
|
|
168
|
+
bun run build # react-native-builder-bob, then verify the published shape
|
|
169
|
+
bun run typescript # type check
|
|
509
170
|
bun run test # jest
|
|
171
|
+
bun run storybook # component workshop on port 6006
|
|
510
172
|
bun run clean # remove lib/
|
|
511
173
|
```
|
|
512
174
|
|
|
175
|
+
`build` regenerates the platform export map and the theme CSS first, then verifies the package it just produced. If that verification fails, the package is wrong, not the check.
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
Issues and pull requests are welcome. Please run `bun run typescript` and `bun run test` first. Org wide [contributing notes](https://github.com/OxyHQ/.github/blob/main/CONTRIBUTING.md), the [security policy](https://github.com/OxyHQ/.github/blob/main/SECURITY.md) and the [code of conduct](https://github.com/OxyHQ/.github/blob/main/CODE_OF_CONDUCT.md) live in the organisation profile.
|
|
180
|
+
|
|
513
181
|
## License
|
|
514
182
|
|
|
515
|
-
|
|
183
|
+
Apache-2.0, The Oxy Collective Inc. See [LICENSE](./LICENSE).
|
|
184
|
+
|
|
185
|
+
Bloom moved from AGPL-3.0-only to Apache-2.0 at `0.87.0`. Versions published before that keep AGPL-3.0-only permanently; a licence change binds future versions only.
|
|
186
|
+
|
|
187
|
+
Third party code Bloom derives from, principally the universal toast engine, is credited in [NOTICE](./NOTICE).
|