@oxyhq/bloom 1.0.0 → 1.0.2

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.
Files changed (218) hide show
  1. package/README.md +16 -4
  2. package/docs/accordion.mdx +69 -0
  3. package/docs/activity-heatmap.mdx +71 -0
  4. package/docs/admonition.mdx +77 -0
  5. package/docs/alert-dialog.mdx +75 -0
  6. package/docs/alert.mdx +93 -0
  7. package/docs/animated-check.mdx +53 -0
  8. package/docs/aspect-ratio.mdx +45 -0
  9. package/docs/avatar-group.mdx +91 -0
  10. package/docs/avatar.mdx +126 -0
  11. package/docs/badge.mdx +74 -0
  12. package/docs/benefit-list.mdx +56 -0
  13. package/docs/bottom-sheet.mdx +103 -0
  14. package/docs/button.mdx +218 -0
  15. package/docs/card.mdx +106 -0
  16. package/docs/checkbox.mdx +59 -0
  17. package/docs/chip.mdx +53 -0
  18. package/docs/code.mdx +50 -0
  19. package/docs/combobox.mdx +64 -0
  20. package/docs/command.mdx +85 -0
  21. package/docs/composition-bar.mdx +63 -0
  22. package/docs/connection-dots.mdx +50 -0
  23. package/docs/connection-status.mdx +56 -0
  24. package/docs/content-panel.mdx +62 -0
  25. package/docs/context-menu.mdx +65 -0
  26. package/docs/design-tokens.mdx +254 -0
  27. package/docs/dialog.mdx +277 -0
  28. package/docs/divider.mdx +56 -0
  29. package/docs/dot-grid-meter.mdx +66 -0
  30. package/docs/dropdown-menu.mdx +161 -0
  31. package/docs/error-boundary.mdx +72 -0
  32. package/docs/fab.mdx +67 -0
  33. package/docs/field.mdx +70 -0
  34. package/docs/fill.mdx +58 -0
  35. package/docs/fonts.mdx +144 -0
  36. package/docs/frosted-icon-button.mdx +60 -0
  37. package/docs/getting-started.mdx +75 -0
  38. package/docs/grid.mdx +64 -0
  39. package/docs/hooks.mdx +81 -0
  40. package/docs/icon-circle.mdx +66 -0
  41. package/docs/icons.mdx +69 -0
  42. package/docs/image-aspect-ratio-cache.mdx +63 -0
  43. package/docs/image-resolver.mdx +66 -0
  44. package/docs/index.mdx +44 -0
  45. package/docs/input-group.mdx +58 -0
  46. package/docs/item.mdx +69 -0
  47. package/docs/kbd.mdx +57 -0
  48. package/docs/label.mdx +50 -0
  49. package/docs/link-preview.mdx +66 -0
  50. package/docs/list.mdx +72 -0
  51. package/docs/loading.mdx +129 -0
  52. package/docs/media-inset-border.mdx +52 -0
  53. package/docs/menubar.mdx +86 -0
  54. package/docs/migrating-to-1.0.mdx +434 -0
  55. package/docs/motion.mdx +83 -0
  56. package/docs/overlay.mdx +75 -0
  57. package/docs/popover.mdx +73 -0
  58. package/docs/portal.mdx +63 -0
  59. package/docs/pressable-scale.mdx +51 -0
  60. package/docs/profile-card.mdx +66 -0
  61. package/docs/progressive-blur.mdx +54 -0
  62. package/docs/prompt-input.mdx +183 -0
  63. package/docs/provider.mdx +64 -0
  64. package/docs/radio-indicator.mdx +60 -0
  65. package/docs/radio.mdx +65 -0
  66. package/docs/scroll.mdx +66 -0
  67. package/docs/search.mdx +67 -0
  68. package/docs/segmented-control.mdx +76 -0
  69. package/docs/select.mdx +93 -0
  70. package/docs/settings-list.mdx +186 -0
  71. package/docs/skeleton.mdx +66 -0
  72. package/docs/slider.mdx +73 -0
  73. package/docs/stat-bar.mdx +59 -0
  74. package/docs/stat-widgets.mdx +33 -0
  75. package/docs/styles.mdx +61 -0
  76. package/docs/subtle-hover.mdx +53 -0
  77. package/docs/surfaces.mdx +72 -0
  78. package/docs/switch.mdx +90 -0
  79. package/docs/tab-bar.mdx +386 -0
  80. package/docs/tabs.mdx +75 -0
  81. package/docs/text-field.mdx +187 -0
  82. package/docs/theme.mdx +67 -0
  83. package/docs/toast.mdx +285 -0
  84. package/docs/tooltip.mdx +67 -0
  85. package/docs/typography.mdx +88 -0
  86. package/docs/user-hover-card.mdx +140 -0
  87. package/docs/zoomable-image-gallery.mdx +66 -0
  88. package/lib/commonjs/design-tokens/color-roles.js +21 -1
  89. package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
  90. package/lib/commonjs/design-tokens/index.js +6 -0
  91. package/lib/commonjs/design-tokens/index.js.map +1 -1
  92. package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
  93. package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
  94. package/lib/commonjs/design-tokens/theme-css.js +1 -0
  95. package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
  96. package/lib/commonjs/design-tokens/theme.css +3 -0
  97. package/lib/commonjs/design-tokens/tokens-json.js +13 -4
  98. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
  99. package/lib/commonjs/design-tokens/tokens.json +6227 -265
  100. package/lib/commonjs/index.js +7 -0
  101. package/lib/commonjs/index.js.map +1 -1
  102. package/lib/commonjs/index.web.js +7 -0
  103. package/lib/commonjs/index.web.js.map +1 -1
  104. package/lib/commonjs/theme/ambient-store.js +16 -1
  105. package/lib/commonjs/theme/ambient-store.js.map +1 -1
  106. package/lib/commonjs/theme/color-policy.js +30 -33
  107. package/lib/commonjs/theme/color-policy.js.map +1 -1
  108. package/lib/commonjs/theme/color-presets.js +413 -166
  109. package/lib/commonjs/theme/color-presets.js.map +1 -1
  110. package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
  111. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  112. package/lib/commonjs/theme/glass-colors.js +18 -21
  113. package/lib/commonjs/theme/glass-colors.js.map +1 -1
  114. package/lib/commonjs/theme/index.js +30 -0
  115. package/lib/commonjs/theme/index.js.map +1 -1
  116. package/lib/commonjs/theme/index.web.js +30 -0
  117. package/lib/commonjs/theme/index.web.js.map +1 -1
  118. package/lib/module/design-tokens/color-roles.js +20 -0
  119. package/lib/module/design-tokens/color-roles.js.map +1 -1
  120. package/lib/module/design-tokens/index.js +2 -2
  121. package/lib/module/design-tokens/index.js.map +1 -1
  122. package/lib/module/design-tokens/tailwind-preset.js +4 -2
  123. package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
  124. package/lib/module/design-tokens/theme-css.js +2 -1
  125. package/lib/module/design-tokens/theme-css.js.map +1 -1
  126. package/lib/module/design-tokens/theme.css +3 -0
  127. package/lib/module/design-tokens/tokens-json.js +14 -5
  128. package/lib/module/design-tokens/tokens-json.js.map +1 -1
  129. package/lib/module/design-tokens/tokens.json +6227 -265
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/index.web.js +1 -1
  133. package/lib/module/index.web.js.map +1 -1
  134. package/lib/module/theme/ambient-store.js +16 -1
  135. package/lib/module/theme/ambient-store.js.map +1 -1
  136. package/lib/module/theme/color-policy.js +30 -33
  137. package/lib/module/theme/color-policy.js.map +1 -1
  138. package/lib/module/theme/color-presets.js +412 -165
  139. package/lib/module/theme/color-presets.js.map +1 -1
  140. package/lib/module/theme/color-scope/seed-scope.js +2 -1
  141. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  142. package/lib/module/theme/glass-colors.js +18 -21
  143. package/lib/module/theme/glass-colors.js.map +1 -1
  144. package/lib/module/theme/index.js +1 -1
  145. package/lib/module/theme/index.js.map +1 -1
  146. package/lib/module/theme/index.web.js +1 -1
  147. package/lib/module/theme/index.web.js.map +1 -1
  148. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
  149. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
  151. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
  155. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/index.d.ts +2 -2
  157. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  159. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
  163. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  165. package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
  166. package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  168. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
  170. package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
  171. package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
  172. package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
  173. package/lib/typescript/module/design-tokens/index.d.ts +3 -3
  174. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  175. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
  176. package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
  177. package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
  178. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
  179. package/lib/typescript/module/index.d.ts +2 -2
  180. package/lib/typescript/module/index.d.ts.map +1 -1
  181. package/lib/typescript/module/index.web.d.ts +2 -2
  182. package/lib/typescript/module/index.web.d.ts.map +1 -1
  183. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
  184. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  185. package/lib/typescript/module/theme/color-presets.d.ts +386 -62
  186. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  187. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  188. package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
  189. package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
  190. package/lib/typescript/module/theme/index.d.ts +2 -2
  191. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  192. package/lib/typescript/module/theme/index.web.d.ts +2 -2
  193. package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
  194. package/package.json +5 -2
  195. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  196. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  197. package/src/__tests__/support/press-host.ts +30 -0
  198. package/src/__tests__/support/rendered-style.ts +99 -0
  199. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  200. package/src/design-tokens/color-roles.ts +21 -0
  201. package/src/design-tokens/index.ts +3 -2
  202. package/src/design-tokens/tailwind-preset.ts +3 -2
  203. package/src/design-tokens/theme-css.ts +2 -1
  204. package/src/design-tokens/theme.css +3 -0
  205. package/src/design-tokens/tokens-json.ts +29 -9
  206. package/src/design-tokens/tokens.json +6227 -265
  207. package/src/glass/GlassSurface.tsx +2 -2
  208. package/src/index.ts +2 -0
  209. package/src/index.web.ts +2 -0
  210. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
  211. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  212. package/src/theme/ambient-store.ts +21 -1
  213. package/src/theme/color-policy.ts +33 -38
  214. package/src/theme/color-presets.ts +473 -146
  215. package/src/theme/color-scope/seed-scope.ts +2 -1
  216. package/src/theme/glass-colors.ts +18 -21
  217. package/src/theme/index.ts +16 -1
  218. package/src/theme/index.web.ts +16 -1
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: FrostedIconButton
3
+ description: A translucent icon button for use over media, where a normal button has nothing to sit on.
4
+ order: 65
5
+ ---
6
+
7
+ # FrostedIconButton
8
+
9
+ A circular icon button with a blurred, translucent background. It is for
10
+ controls that float **over content** — a close button on a photo viewer, a
11
+ mute toggle on a video, a back button over a hero image.
12
+
13
+ ```tsx
14
+ import { FrostedIconButton } from '@oxyhq/bloom/frosted-icon-button';
15
+
16
+ <FrostedIconButton
17
+ icon={<CloseIcon />}
18
+ accessibilityLabel="Close"
19
+ onPress={close}
20
+ />
21
+ ```
22
+
23
+ ## Which prop decides what
24
+
25
+ | prop | decides |
26
+ | ---------- | ---------------------------------------------------- |
27
+ | `icon` | the glyph |
28
+ | `size` | `sm` · `md`, or a number |
29
+ | `active` | a persistent on state, for a toggle |
30
+ | `disabled` | dims and disables |
31
+ | `hitSlop` | the touch target beyond the drawn circle |
32
+
33
+ ## Why it exists rather than a `Button` variant
34
+
35
+ Over arbitrary media there is no background to be legible against: a photo can
36
+ be white here and black there. The frosted fill blurs whatever is behind it so
37
+ the glyph keeps its contrast wherever the button lands, which is not something
38
+ a solid or ghost button can do.
39
+
40
+ That also means it is the wrong choice **on** a surface. Over a card or a page
41
+ background, frosting blurs a flat colour — it costs a blur pass to look like a
42
+ slightly muddy circle. Use `Button`'s icon form there.
43
+
44
+ ## Always give it a label
45
+
46
+ An icon-only button announces as "button" and nothing else. `accessibilityLabel`
47
+ is not decoration here; it is the entire accessible name.
48
+
49
+ ## `hitSlop` for small buttons
50
+
51
+ A 32px circle over media is usually near a screen edge and pressed with a
52
+ thumb. `hitSlop` grows the target without growing the drawn button, which is
53
+ preferable to drawing something bigger than the design calls for.
54
+
55
+ ## Web-only props
56
+
57
+ `onClick`, `title`, `id`, `type` and `aria-label` are on the shared type so one
58
+ call site type-checks on both platforms. They are consumed by the web fork,
59
+ which renders a real DOM `<button>`, and are no-ops on native. Prefer
60
+ `onPress` and `accessibilityLabel`, which work on both.
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Getting started
3
+ description: Install Bloom and wrap your app with the theme and dialog providers.
4
+ order: 1
5
+ ---
6
+
7
+ # Getting started
8
+
9
+ ## 1. Install
10
+
11
+ ```sh
12
+ bun add @oxyhq/bloom
13
+ ```
14
+
15
+ Install the peer dependencies. The same set covers native and web — Bloom's
16
+ overlays and its toast engine run on react-native-web:
17
+
18
+ ```sh
19
+ bun add react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg
20
+ ```
21
+
22
+ ## 2. Wrap your app root
23
+
24
+ ```tsx
25
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
26
+ import { BloomThemeProvider, SurfaceProvider } from '@oxyhq/bloom';
27
+
28
+ export default function Root() {
29
+ return (
30
+ <GestureHandlerRootView style={{ flex: 1 }}>
31
+ <BloomThemeProvider mode="system" colorPreset="teal">
32
+ <SurfaceProvider>
33
+ <App />
34
+ </SurfaceProvider>
35
+ </BloomThemeProvider>
36
+ </GestureHandlerRootView>
37
+ );
38
+ }
39
+ ```
40
+
41
+ - `BloomThemeProvider` is required — every Bloom component reads from it.
42
+ - `SurfaceProvider` (or a bare `<SurfaceHost />`) is required only if you use the imperative surfaces — `alert()`, `confirm()`, `prompt()`, `surfaces.present()`. Without it, `<Dialog>` still works on its own.
43
+ - `GestureHandlerRootView` is required on native for any sheet-based component.
44
+
45
+ <Callout variant="warning" title="Web setup">
46
+ Inject the CSS animations into your global styles once:
47
+
48
+ ```tsx
49
+ import { BLOOM_DIALOG_CSS } from '@oxyhq/bloom/dialog';
50
+
51
+ // In your HTML head or global CSS file:
52
+ <style>{BLOOM_DIALOG_CSS}</style>
53
+ ```
54
+ </Callout>
55
+
56
+ ## 3. Use a component
57
+
58
+ ```tsx
59
+ import { Button } from '@oxyhq/bloom/button';
60
+ import { toast } from '@oxyhq/bloom';
61
+
62
+ export function Hello() {
63
+ return <Button onPress={() => toast.success('Hello, Bloom!')}>Greet</Button>;
64
+ }
65
+ ```
66
+
67
+ ## Recommended layout
68
+
69
+ If your app supports both light and dark themes:
70
+
71
+ ```tsx
72
+ <BloomThemeProvider mode="system" colorPreset="teal">
73
+ ```
74
+
75
+ `mode="system"` follows the OS preference. `colorPreset` picks one of the 10 named palettes — see [Theme](/docs/bloom/main/theme) for the full list.
package/docs/grid.mdx ADDED
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Grid
3
+ description: Row and Col — a gutter that keeps the grid's outer edges flush with everything else.
4
+ order: 45
5
+ ---
6
+
7
+ # Grid
8
+
9
+ `Row` and `Col` are a twelfth-free fractional grid. A row holds columns; each
10
+ column takes a fraction of the width.
11
+
12
+ ```tsx
13
+ import { Row, Col } from '@oxyhq/bloom/grid';
14
+
15
+ <Row gap={16}>
16
+ <Col width={2 / 3}>
17
+ <Article />
18
+ </Col>
19
+ <Col width={1 / 3}>
20
+ <Sidebar />
21
+ </Col>
22
+ </Row>
23
+ ```
24
+
25
+ ## Which prop decides what
26
+
27
+ | prop | decides |
28
+ | ------- | ----------------------------------------------------------- |
29
+ | `gap` | on `Row`, the space between columns |
30
+ | `width` | on `Col`, its fraction of the row — `0.5`, `1 / 3`, `0.25` |
31
+
32
+ ## How the gutter works, and why it matters
33
+
34
+ The row pulls itself out by **half** the gap on each side, and each column pads
35
+ in by half. The result is that the outer edges of the grid line up with the
36
+ content above and below it, while the columns are still separated.
37
+
38
+ A grid that only padded its columns would be inset by half a gutter on both
39
+ sides — a few pixels of misalignment against every other block on the page,
40
+ which is the kind of thing that looks like nothing and reads as sloppy.
41
+
42
+ That is also why a `Col` outside any `Row` gets no gutter at all rather than a
43
+ default one: without a row there is no negative margin to compensate for it.
44
+
45
+ ## `width` is a fraction, not a column count
46
+
47
+ `width={1 / 3}` rather than `width={4}` out of twelve. Fractions compose with
48
+ any number of columns and do not require the reader to remember the divisor.
49
+ `1 / 3` is exactly what it says; `4` is only meaningful once you know the grid
50
+ is twelve wide.
51
+
52
+ ## Grid or flexbox
53
+
54
+ `Row`/`Col` is for a **proportional split** — two thirds and one third,
55
+ regardless of content. Ordinary flexbox is better when the split follows the
56
+ content (a label that hugs its text beside a field that takes the rest), and a
57
+ `gap` on a plain `View` is better when everything is the same size.
58
+
59
+ ## Wrapping
60
+
61
+ `Row` does not wrap. A responsive layout switches between one column and two by
62
+ rendering different structures at the breakpoint, rather than by letting a row
63
+ reflow — a wrapped grid loses the alignment the gutter maths exists to
64
+ preserve.
package/docs/hooks.mdx ADDED
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Hooks
3
+ description: The shared behaviours Bloom's own components are built on.
4
+ order: 73
5
+ ---
6
+
7
+ # Hooks
8
+
9
+ `@oxyhq/bloom/hooks` is the set of behaviours Bloom's components share. They
10
+ are exported because apps hit the same problems, and because a second copy of
11
+ any of them drifts.
12
+
13
+ ```tsx
14
+ import { useInteractionState, useDelayedLoading, useGutters } from '@oxyhq/bloom/hooks';
15
+ ```
16
+
17
+ ## `useInteractionState`
18
+
19
+ Press and hover state as plain booleans, with the handlers to wire up.
20
+
21
+ ```tsx
22
+ const { state: pressed, onIn, onOut } = useInteractionState();
23
+
24
+ <Pressable onPressIn={onIn} onPressOut={onOut} style={[base, pressed && dim]}>
25
+ ```
26
+
27
+ Bloom uses this rather than `Pressable`'s function-form `style`, which
28
+ NativeWind's css-interop swallows — taking the base container style (background,
29
+ radius, border, shadow) with it. That failure is silent and total: the component
30
+ renders unstyled and nothing errors.
31
+
32
+ ## `useDelayedLoading`
33
+
34
+ Holds a flag true for a fixed delay, then flips it false.
35
+
36
+ ```tsx
37
+ const settling = useDelayedLoading(300); // true for 300ms, then false
38
+ const showContent = !settling && !isFetching;
39
+ ```
40
+
41
+ It takes the DELAY, not the loading flag: `useDelayedLoading(delay, initialState?)`.
42
+ Use it to guarantee a minimum on-screen time. A spinner that appears for 80ms
43
+ and vanishes is worse than no spinner — the flash reads as a glitch, and the
44
+ user cannot tell what happened.
45
+
46
+ ## `useThrottledValue`
47
+
48
+ A value that updates at most every N ms. For anything that re-renders a
49
+ expensive tree on a continuous input — a drag, a scroll position, a typed
50
+ query.
51
+
52
+ ## `usePressAnimation`
53
+
54
+ The shared press transform, so a press dip is the same everywhere.
55
+
56
+ ## `useGutters`
57
+
58
+ The screen's horizontal insets, from one place, so a header and the content
59
+ below it agree about where the margin is.
60
+
61
+ ## `useHaptics`
62
+
63
+ Haptic feedback through Bloom's optional-peer boundary — a no-op when
64
+ `expo-haptics` is not installed, rather than a build failure.
65
+
66
+ Mount `BloomHapticsProvider` if the app needs to turn haptics off globally
67
+ (a user setting, or a platform where they are inappropriate).
68
+
69
+ ## `useImagePreload` / `preloadImage`
70
+
71
+ Fetch an image before it is rendered — the transition into a full-screen viewer,
72
+ the next page of a carousel.
73
+
74
+ ## `mergeRefs`
75
+
76
+ Combines several refs into one callback.
77
+
78
+ Note the return type: `(instance: T | null) => void`, **not**
79
+ `React.RefCallback<T>`. That is deliberate — the plain function type stays
80
+ assignable across duplicate copies of `@types/react` in a consumer's tree,
81
+ which the branded type does not.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: IconCircle
3
+ description: An icon on a tinted disc — the reference for how Bloom tints anything.
4
+ order: 47
5
+ ---
6
+
7
+ # IconCircle
8
+
9
+ An icon centred on a coloured disc. Empty states, consent steps, the header of
10
+ a settings section — anywhere an icon needs presence rather than sitting beside
11
+ text.
12
+
13
+ ```tsx
14
+ import { IconCircle } from '@oxyhq/bloom/icon-circle';
15
+ import { Shield_Stroke2_Corner0_Rounded as ShieldIcon } from '@oxyhq/bloom/icons';
16
+
17
+ <IconCircle icon={ShieldIcon} />
18
+ <IconCircle icon={ShieldIcon} size="lg" />
19
+ ```
20
+
21
+ ## Which prop decides what
22
+
23
+ | prop | decides |
24
+ | ----------- | ---------------------------------------------- |
25
+ | `icon` | the glyph component — passed, not rendered |
26
+ | `size` | `lg` (52px) or `xl` (64px, the default) |
27
+ | `style` | the disc |
28
+ | `iconStyle` | the glyph |
29
+
30
+ `icon` takes the component itself (`icon={ShieldIcon}`), not an element
31
+ (`icon={<ShieldIcon />}`), so the circle can size and colour it.
32
+
33
+ ## The tinting rule this component is the reference for
34
+
35
+ The disc is `primarySubtle` and the glyph is `primary` — a **pair** of resolved
36
+ tokens, not one token with alpha appended.
37
+
38
+ That distinction is the whole point. A Bloom accent token resolves to a full
39
+ `rgb(...)`, so `` `${colors.primary}1A` `` is a malformed string that
40
+ react-native-web parses back as fully **opaque**. A disc tinted that way ends
41
+ up the same colour as the glyph on it — contrast 1.00, an invisible icon, and
42
+ markup that reads as correct.
43
+
44
+ ## Override both or neither
45
+
46
+ `style` moves the disc, `iconStyle` moves the glyph. Changing one is exactly
47
+ how a tinted disc ends up with an invisible icon:
48
+
49
+ ```tsx
50
+ <IconCircle
51
+ icon={BellIcon}
52
+ style={{ backgroundColor: 'rgb(255 237 213)' }}
53
+ iconStyle={{ color: 'rgb(154 52 18)' }}
54
+ />
55
+ ```
56
+
57
+ If you find yourself doing this often for the same tone, that is a signal the
58
+ tone belongs in the theme — `resolveAccentColors(colors, tone, fill)` reads the
59
+ `*Subtle` / `*SubtleForeground` pairs the design tokens already gate together.
60
+
61
+ ## Not a button
62
+
63
+ `IconCircle` draws; it does not press. Wrap it in a `Pressable` with an
64
+ `accessibilityLabel` if it should do something — and consider
65
+ [`FrostedIconButton`](/docs/bloom/main/frosted-icon-button) or `Button`'s icon
66
+ form first, which are already controls.
package/docs/icons.mdx ADDED
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Icons
3
+ description: 250-odd glyphs with full names — and why a shortened one is invisible to the compiler.
4
+ order: 66
5
+ ---
6
+
7
+ # Icons
8
+
9
+ Bloom's icon set. Every glyph is a named export carrying its style and corner
10
+ suffix.
11
+
12
+ ```tsx
13
+ import { Bell_Stroke2_Corner0_Rounded as BellIcon } from '@oxyhq/bloom/icons';
14
+
15
+ <BellIcon size="lg" fill={colors.text} />
16
+ ```
17
+
18
+ ## The naming is `<Subject>_<Style>_Corner0_Rounded`
19
+
20
+ The suffix is not decoration — it is *which drawing you get*. `Stroke2` and
21
+ `Filled` are separate exports rather than a prop on one component, because they
22
+ are different artwork: filled reads as "on"/selected, stroke as the resting
23
+ state. A tab bar uses both for the same subject.
24
+
25
+ Import the exact identifier and alias it locally, as above.
26
+
27
+ ## `fill` and `size`, not inheritance
28
+
29
+ Colour travels on `fill` and size on `size`. Neither is inherited from a
30
+ surrounding `Text`, so an icon beside a label has to be told both — an icon
31
+ that "looks black in dark mode" is almost always one that was never given a
32
+ `fill`.
33
+
34
+ ## A shortened name is invisible to `tsc`
35
+
36
+ Dropping the suffix — writing the bare subject where the full identifier
37
+ belongs — is not a compile error anywhere such a reference usually appears: in
38
+ a **doc comment** or an `.mdx` example, TypeScript never sees it. A reader
39
+ copies it, gets `undefined`, and React reports "Element type is invalid … but
40
+ got: undefined" against *their* file — which is how a phantom release blocker
41
+ once got filed against the tab bar.
42
+
43
+ `src/__tests__/icon-references.test.ts` scans `src/`, `docs/`, `README.md` and
44
+ `AGENTS.md` for references that do not resolve against the real export union.
45
+ It is the only gate on this class of bug, and it deliberately does **not** strip
46
+ comments, because the comments are the surface under test.
47
+
48
+ ## Two import shapes are correct, one is not
49
+
50
+ ```tsx
51
+ import * as Icons from '@oxyhq/bloom/icons'; // ✅ the subpath, as a namespace
52
+ import { Icons } from '@oxyhq/bloom'; // ✅ the root barrel's namespace
53
+ ```
54
+
55
+ The third shape — importing `Icons` as a *named* export from the **subpath** —
56
+ is `undefined` at runtime. The subpath exports the components flat; the `Icons`
57
+ name is assembled by the root barrel (`export * as Icons from './icons'`), so
58
+ there is no such named export to destructure.
59
+
60
+ The gate below flags that shape too, which is why this page describes it rather
61
+ than showing it: a counter-example spelled as a literal is indistinguishable
62
+ from the mistake it warns about, and the scan is right to flag both.
63
+
64
+ ## Adding an icon
65
+
66
+ One file per subject under `src/icons/`, exporting the full identifiers, and
67
+ re-exported from the barrel. Keep the stroke and filled variants together in
68
+ the same file — they are the same subject, and separating them is how one gets
69
+ updated without the other.
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: Image aspect-ratio cache
3
+ description: Remembering an image's shape so a list stops jumping when it loads.
4
+ order: 67
5
+ ---
6
+
7
+ # Image aspect-ratio cache
8
+
9
+ A process-wide cache of image intrinsic sizes, keyed by URL.
10
+
11
+ ```tsx
12
+ import {
13
+ getAspectRatio,
14
+ hasAspectRatio,
15
+ fetchAspectRatio,
16
+ setAspectRatio,
17
+ DEFAULT_ASPECT_RATIO,
18
+ } from '@oxyhq/bloom/image-aspect-ratio-cache';
19
+
20
+ const ratio = hasAspectRatio(url) ? getAspectRatio(url) : DEFAULT_ASPECT_RATIO;
21
+ ```
22
+
23
+ ## The problem it solves
24
+
25
+ An image's shape is not known until it loads. A list that reserves no space
26
+ reflows every time one arrives, which throws the reading position down the page
27
+ — the classic feed that jumps while you are reading it.
28
+
29
+ Reserving a *fixed* box instead is not a fix: it letterboxes portrait images and
30
+ crops landscape ones. What you want is the real ratio, remembered, so the second
31
+ time the same image appears there is no jump at all.
32
+
33
+ ## The functions
34
+
35
+ | function | for |
36
+ | ----------------------- | ----------------------------------------------------- |
37
+ | `hasAspectRatio(url)` | whether the shape is known **without** guessing |
38
+ | `getAspectRatio(url)` | the ratio, if known |
39
+ | `getIntrinsicSize(url)` | the width and height, if known |
40
+ | `fetchAspectRatio(url)` | measure it and cache the result |
41
+ | `setAspectRatio(url, …)`| record one you already know — from a DTO, say |
42
+ | `DEFAULT_ASPECT_RATIO` | what to reserve while it is unknown |
43
+
44
+ ## `hasAspectRatio` is not the same as `getAspectRatio() !== undefined`
45
+
46
+ Keep the distinction. "Not measured yet" and "measured, and it happens to be
47
+ the default" want different behaviour: the first should reserve the default and
48
+ then settle, the second is already correct and must not animate.
49
+
50
+ ## Prefer `setAspectRatio` over measuring
51
+
52
+ If the API already returns the dimensions — most media DTOs do — record them
53
+ and skip the measurement entirely. A ratio known before first paint means the
54
+ layout is right on frame one, which no amount of caching after the fact
55
+ achieves.
56
+
57
+ ## It is a cache, not state
58
+
59
+ Module-level and process-wide, so it survives navigation and re-renders. Do not
60
+ read it in a memoized position: it is external mutable state, and under the
61
+ React Compiler that is exactly the shape that produces a stale read. Read it
62
+ during render for the initial reservation, and drive updates from the load
63
+ event.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: ImageResolver
3
+ description: The one chokepoint that turns an Oxy file id into a URL.
4
+ order: 54
5
+ ---
6
+
7
+ # ImageResolver
8
+
9
+ Bloom components take a **file id** and ask the app how to turn it into a URL.
10
+ The app registers that function once, at the root.
11
+
12
+ ```tsx
13
+ import { ImageResolverProvider } from '@oxyhq/bloom/image-resolver';
14
+ import { oxyServices } from '@oxyhq/services';
15
+
16
+ <ImageResolverProvider value={(id, variant) => oxyServices.getFileDownloadUrl(id, variant)}>
17
+ <App />
18
+ </ImageResolverProvider>
19
+ ```
20
+
21
+ ```ts
22
+ type ImageResolver = (id: string, variant?: string) => string | undefined;
23
+ ```
24
+
25
+ ## Why Bloom does not build the URL itself
26
+
27
+ A URL builder in Bloom would hardcode a host, a path shape and a variant
28
+ vocabulary that belong to the platform, not to a component library — and every
29
+ consumer would then need Bloom released to change any of them. One function,
30
+ registered by the app, keeps the media contract in `@oxyhq/services` where it
31
+ already lives.
32
+
33
+ That is also why there is no `avatarUrl` field on any DTO and no per-app URL
34
+ helper: **one chokepoint**, `getFileDownloadUrl(id, variant)`, plus this
35
+ resolver.
36
+
37
+ ## `source` versus `uri` — the failure that is total and silent
38
+
39
+ `Avatar` invokes the resolver only for a **non-URL string** passed as `source`.
40
+
41
+ ```tsx
42
+ <Avatar source={user.avatar} /> // ✅ file id → resolver → URL
43
+ <Avatar uri={user.avatar} /> // ✗ file id used as a raw URL → 404
44
+ ```
45
+
46
+ A full URL or an `{ uri }` object passes through untouched, so both props
47
+ "work" in a test with a real URL and only one of them works with a file id.
48
+ Nothing throws — the image 404s and the avatar falls back to its initial. The
49
+ symptom is *every avatar shows a placeholder character*, which reads as missing
50
+ data rather than as a wiring mistake.
51
+
52
+ The same applies to every Bloom component that wraps `Avatar`:
53
+ [`AvatarGroup`](/docs/bloom/main/avatar-group) items and
54
+ [`UserHoverCard`](/docs/bloom/main/user-hover-card).
55
+
56
+ ## `variant`
57
+
58
+ The second argument asks for a size. `Avatar` defaults it to `thumb` rather
59
+ than requesting the full-size original for a 32px circle — a detail worth
60
+ keeping if you write a resolver that ignores the argument.
61
+
62
+ ## Register it once
63
+
64
+ One provider at the app root. A second one deeper in the tree silently shadows
65
+ the first for that subtree, which produces images that work on some screens and
66
+ not others.
package/docs/index.mdx ADDED
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Bloom
3
+ description: Shared UI component library for the Oxy ecosystem. React Native + Expo + Web.
4
+ order: 0
5
+ ---
6
+
7
+ # Bloom
8
+
9
+ Shared UI component library for the Oxy ecosystem. Built for React Native + Expo + Web.
10
+
11
+ <Badge>Apache-2.0</Badge>
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ bun add @oxyhq/bloom
17
+ ```
18
+
19
+ ### Peer dependencies
20
+
21
+ Required:
22
+
23
+ - `react >= 18`
24
+ - `react-native >= 0.73`
25
+ - `react-native-safe-area-context >= 5`
26
+
27
+ Also required:
28
+
29
+ - `react-native-reanimated >= 3.13` (`Dialog`, `BottomSheet`, `toast`, `Loading`) — on web too
30
+ - `react-native-gesture-handler >= 2.16.1` (`Dialog`, `BottomSheet`, toast swipe-to-dismiss) — native also needs the app root wrapped in `GestureHandlerRootView`
31
+ - `react-native-svg >= 13` (Bloom icons, Avatar `squircle` shape)
32
+
33
+ ## What's inside
34
+
35
+ Bloom ships modal primitives, form controls, layout helpers, typography, theming, and a portal system that works on both native and web. Everything is exported under a sub-path (`@oxyhq/bloom/dialog`, `@oxyhq/bloom/button`, etc.) so tree-shaking stays effective.
36
+
37
+ ## Next steps
38
+
39
+ - [Getting started](/docs/bloom/main/getting-started) — wire up `BloomThemeProvider` and `SurfaceProvider`.
40
+ - [Theme](/docs/bloom/main/theme) — color presets, modes, design tokens.
41
+ - [Dialog](/docs/bloom/main/dialog) — the unified Dialog API.
42
+ - [BottomSheet](/docs/bloom/main/bottom-sheet) — gesture-driven sheet primitive.
43
+ - [Toast](/docs/bloom/main/toast) — passive notifications.
44
+ - [alert()](/docs/bloom/main/alert) — imperative one-shot confirmations.
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: InputGroup
3
+ description: A control with things attached to it — one bordered box, not several.
4
+ order: 34
5
+ ---
6
+
7
+ # InputGroup
8
+
9
+ An input with prefixes, suffixes or buttons attached, drawn as **one** bordered
10
+ box so the whole thing reads as a single control.
11
+
12
+ ```tsx
13
+ import { InputGroup, InputGroupAddon } from '@oxyhq/bloom/input-group';
14
+
15
+ <InputGroup>
16
+ <InputGroupAddon>oxy.so/</InputGroupAddon>
17
+ <TextFieldInput label="Handle" value={handle} onChangeText={setHandle} />
18
+ </InputGroup>
19
+
20
+ <InputGroup>
21
+ <TextFieldInput label="Search" value={query} onChangeText={setQuery} />
22
+ <InputGroupAddon divider>
23
+ <Button size="small" onPress={run}>Search</Button>
24
+ </InputGroupAddon>
25
+ </InputGroup>
26
+ ```
27
+
28
+ ## Which prop decides what
29
+
30
+ | prop | decides |
31
+ | ----------- | -------------------------------------------------------------- |
32
+ | `size` | `sm` · `md` · `lg` — applied to the group, so parts agree |
33
+ | `isInvalid` | the error border on the whole box |
34
+ | `disabled` | dims and disables the group |
35
+ | `divider` | per addon, a rule between it and the input |
36
+ | `noPadding` | per addon, for an addon that supplies its own (a button) |
37
+
38
+ ## Why the state lives on the group
39
+
40
+ `isInvalid` and `disabled` are properties of the **control**, not of the input
41
+ element inside it. Setting them per part is how a box ends up with a red border
42
+ and a normal-coloured button, or an enabled button beside a disabled input.
43
+
44
+ ## A prefix is not a placeholder and not a label
45
+
46
+ `oxy.so/` as an addon is part of the value's shape: it is always visible, it
47
+ does not disappear on typing, and it is not editable. That is different from a
48
+ placeholder (an example, which vanishes) and from a label (the name of the
49
+ field, which belongs to [`Field`](/docs/bloom/main/field) or
50
+ [`Label`](/docs/bloom/main/label)).
51
+
52
+ `InputGroup` goes *inside* a `Field` — the group is the control, the field is
53
+ its label, hint and error.
54
+
55
+ ## `divider`
56
+
57
+ Use it when the addon is a distinct target — a button, a dropdown. Skip it for
58
+ a static prefix or an icon, where a rule makes one control look like two.