@oxyhq/bloom 0.86.0 → 0.87.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/NOTICE CHANGED
@@ -1,5 +1,8 @@
1
1
  @oxyhq/bloom
2
- Copyright (c) Oxy HQ
2
+ Copyright (c) 2026-present The Oxy Collective, Inc.
3
+
4
+ This product is licensed under the Apache License, Version 2.0. See the LICENSE
5
+ file.
3
6
 
4
7
  This product includes software derived from third-party open-source projects.
5
8
 
package/README.md CHANGED
@@ -1,515 +1,187 @@
1
- # Bloom
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
- Shared UI component library for the Oxy ecosystem. Built for React Native + Expo + Web.
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
- ## Install
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
- ```tsx
176
- <Dialog control={control} title="Pick a tag" contentPadding={0}>
177
- <YourCustomBody />
178
- </Dialog>
179
- ```
17
+ <table>
18
+ <tr>
19
+ <td valign="top" width="50%">
180
20
 
181
- #### Pure custom
21
+ ### What it is
182
22
 
183
- Drop the declarative props entirely `children` owns every pixel.
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
- ```tsx
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
- #### Props
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
- ```tsx
224
- import { BLOOM_DIALOG_CSS } from '@oxyhq/bloom/dialog';
30
+ ### How it fits the Oxy platform
225
31
 
226
- // In your HTML head or global CSS file:
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
- Passive notifications. Bloom's own universal engine one implementation on native and web, no `sonner` / `sonner-native` to install.
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
- // Mount once near the app root, inside BloomThemeProvider:
239
- <ToastOutlet />
36
+ </td>
37
+ </tr>
38
+ </table>
240
39
 
241
- // Anywhere in your app:
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
- const id = toast.loading('Uploading…');
249
- toast.dismiss(id);
42
+ ```bash
43
+ bun add @oxyhq/bloom
250
44
  ```
251
45
 
252
- `toast(content, options?)` accepts strings or React elements, and every method
253
- returns the toast's id. `options.type` (`'default' | 'success' | 'error' |
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
- ### alert()
49
+ <br>
259
50
 
260
- Imperative one-shot confirmation dialogs, matching React Native's `Alert.alert(title, message?, buttons?)` signature. Calls are queued and rendered through `BloomDialogProvider` — you can call `alert()` from anywhere, including before the provider mounts (alerts queue and drain on subscribe).
51
+ Always required:
261
52
 
262
- ```tsx
263
- import { alert } from '@oxyhq/bloom';
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
- alert('Sign out?', 'Are you sure you want to sign out of this device?', [
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
- // Single OK button (default when no buttons passed):
271
- alert('Saved');
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
- Each button:
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
- - `text: string` — required label.
277
- - `style?: 'default' | 'cancel' | 'destructive'` — defaults to `'default'`.
278
- - `onPress?: () => void` — fires after the dialog finishes closing.
77
+ </details>
279
78
 
280
- ### BottomSheet
79
+ ## Getting started
281
80
 
282
- A standalone, draggable bottom sheet built on React Native `Modal` + `react-native-reanimated` + `react-native-gesture-handler`. **Not** based on `@gorhom/bottom-sheet`, so it does not require `BottomSheetModalProvider`. Use it when the compound `Dialog` API doesn't fit, when you want to avoid the Gorhom dependency, or when you need direct control over scroll, keyboard handling, or detached presentation.
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 { useRef } from 'react';
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
- ```tsx
334
- <BottomSheet
335
- ref={sheetRef}
336
- scrollable={false}
337
- manualActivation
338
- dynamicBackdrop
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
- <PhotoGrid />
342
- </BottomSheet>
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
- Variants: `primary`, `secondary`, `icon`, `ghost`, `text`. Sizes: `small`, `medium`, `large`.
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
- iOS-settings-style grouped action list.
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 { GroupedButtons } from '@oxyhq/bloom/grouped-buttons';
102
+ import { Button } from '@oxyhq/bloom/button';
103
+ import { toast } from '@oxyhq/bloom/toast';
367
104
 
368
- <GroupedButtons>
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
- ### Divider
108
+ ## Theming
376
109
 
377
- ```tsx
378
- import { Divider } from '@oxyhq/bloom/divider';
110
+ <table>
111
+ <tr>
112
+ <td valign="top" width="50%">
379
113
 
380
- <Divider />
381
- <Divider spacing={16} color="#ccc" />
382
- <Divider vertical />
383
- ```
114
+ **Four modes**
384
115
 
385
- ### RadioIndicator
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 { RadioIndicator } from '@oxyhq/bloom/radio-indicator';
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
- <Avatar uri="https://example.com/photo.jpg" size={48} />
402
- <Avatar uri={userPhoto} shape="squircle" verified verifiedIcon={<BadgeIcon />} />
403
- <Avatar fallbackSource={require('./default.png')} />
121
+ const theme = useTheme();
122
+ // theme.colors.primary, theme.colors.text, theme.isDark
404
123
  ```
405
124
 
406
- ### Loading
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
- <Loading />
414
- <Loading variant="spinner" text="Loading..." />
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
- Animation is platform-specific but the API and visuals are identical. On native, the spinner (used by `spinner`/`inline`) and the `top` collapse/expand are driven by `react-native-reanimated` (with `react-native-svg` for the spinner blades when available). On web, `Loading` resolves to a CSS-animated fork — a `@keyframes` rotation for the spinner and CSS transitions for the `top` variant — so it imports neither reanimated nor SVG and works in any web bundler (Vite, webpack, Metro-web) with no extra peers.
130
+ **Eighteen colour presets**
421
131
 
422
- ### Collapsible
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
- ```tsx
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
- <Collapsible title="Advanced Options" defaultOpen={false}>
428
- <Text>Hidden content here</Text>
429
- </Collapsible>
430
- ```
136
+ </td>
137
+ </tr>
138
+ </table>
431
139
 
432
- ### ErrorBoundary
140
+ ## Components
433
141
 
434
- ```tsx
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
- <ErrorBoundary onError={(error) => logError(error)}>
438
- <App />
439
- </ErrorBoundary>
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
- <ErrorBoundary
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
- ### PromptInput
158
+ ## Documentation
452
159
 
453
- AI chat input with attachments, fullscreen expand, and submit/stop control.
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
- ```tsx
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
- ```sh
166
+ ```bash
506
167
  bun install
507
- bun run build # react-native-builder-bob
508
- bun run typescript # type-check
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
- AGPL-3.0-only -- The Oxy Foundation, Inc. See the [LICENSE](LICENSE) file for details.
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).