@oxyhq/bloom 1.0.0 → 1.0.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.
Files changed (158) 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/tokens-json.js +13 -4
  89. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
  90. package/lib/commonjs/design-tokens/tokens.json +6227 -265
  91. package/lib/commonjs/theme/ambient-store.js +16 -1
  92. package/lib/commonjs/theme/ambient-store.js.map +1 -1
  93. package/lib/commonjs/theme/color-policy.js +30 -33
  94. package/lib/commonjs/theme/color-policy.js.map +1 -1
  95. package/lib/commonjs/theme/color-presets.js +413 -166
  96. package/lib/commonjs/theme/color-presets.js.map +1 -1
  97. package/lib/commonjs/theme/glass-colors.js +18 -21
  98. package/lib/commonjs/theme/glass-colors.js.map +1 -1
  99. package/lib/commonjs/theme/index.js +30 -0
  100. package/lib/commonjs/theme/index.js.map +1 -1
  101. package/lib/commonjs/theme/index.web.js +30 -0
  102. package/lib/commonjs/theme/index.web.js.map +1 -1
  103. package/lib/module/design-tokens/tokens-json.js +14 -5
  104. package/lib/module/design-tokens/tokens-json.js.map +1 -1
  105. package/lib/module/design-tokens/tokens.json +6227 -265
  106. package/lib/module/theme/ambient-store.js +16 -1
  107. package/lib/module/theme/ambient-store.js.map +1 -1
  108. package/lib/module/theme/color-policy.js +30 -33
  109. package/lib/module/theme/color-policy.js.map +1 -1
  110. package/lib/module/theme/color-presets.js +412 -165
  111. package/lib/module/theme/color-presets.js.map +1 -1
  112. package/lib/module/theme/glass-colors.js +18 -21
  113. package/lib/module/theme/glass-colors.js.map +1 -1
  114. package/lib/module/theme/index.js +1 -1
  115. package/lib/module/theme/index.js.map +1 -1
  116. package/lib/module/theme/index.web.js +1 -1
  117. package/lib/module/theme/index.web.js.map +1 -1
  118. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
  119. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
  123. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
  125. package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  127. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
  129. package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
  130. package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
  131. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
  132. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
  133. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  134. package/lib/typescript/module/theme/color-presets.d.ts +386 -62
  135. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  136. package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
  137. package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
  138. package/lib/typescript/module/theme/index.d.ts +2 -2
  139. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  140. package/lib/typescript/module/theme/index.web.d.ts +2 -2
  141. package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
  142. package/package.json +5 -2
  143. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  144. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  145. package/src/__tests__/support/press-host.ts +30 -0
  146. package/src/__tests__/support/rendered-style.ts +99 -0
  147. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  148. package/src/design-tokens/tokens-json.ts +29 -9
  149. package/src/design-tokens/tokens.json +6227 -265
  150. package/src/glass/GlassSurface.tsx +2 -2
  151. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
  152. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  153. package/src/theme/ambient-store.ts +21 -1
  154. package/src/theme/color-policy.ts +33 -38
  155. package/src/theme/color-presets.ts +473 -146
  156. package/src/theme/glass-colors.ts +18 -21
  157. package/src/theme/index.ts +16 -1
  158. package/src/theme/index.web.ts +16 -1
package/README.md CHANGED
@@ -76,6 +76,16 @@ On native, wrap the app root in `GestureHandlerRootView` from `react-native-gest
76
76
 
77
77
  </details>
78
78
 
79
+ ## Upgrading from 0.x
80
+
81
+ Bloom follows the clean cut rule, so there are no `@deprecated` aliases and no old name shims anywhere in 1.0.0. If a symbol you import was renamed, nothing re-exports it under the old spelling and the build fails. **[`docs/migrating-to-1.0.mdx`](./docs/migrating-to-1.0.mdx) is the only bridge** — every removed subpath, every rename, every prop change, derived from the published 0.89.0 and 1.0.0 tarballs rather than from commit messages.
82
+
83
+ That file ships inside the package, so the copy you read is the one that describes the version you installed:
84
+
85
+ ```bash
86
+ $EDITOR node_modules/@oxyhq/bloom/docs/migrating-to-1.0.mdx
87
+ ```
88
+
79
89
  ## Getting started
80
90
 
81
91
  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.
@@ -144,10 +154,10 @@ Bloom publishes 88 subpath exports. Importing from the subpath rather than the r
144
154
  | Group | Exports |
145
155
  |---|---|
146
156
  | 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` |
157
+ | Overlays | `dialog`, `alert-dialog`, `bottom-sheet`, `popover`, `context-menu`, `dropdown-menu`, `menubar`, `tooltip`, `overlay`, `portal`, `surfaces` |
148
158
  | Actions | `button`, `fab`, `frosted-icon-button`, `pressable-scale`, `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`, `divider`, `item` |
159
+ | Forms | `text-field`, `field`, `input-group`, `label`, `select`, `combobox`, `command`, `checkbox`, `radio`, `switch`, `slider`, `segmented-control`, `search`, `prompt-input` |
160
+ | Layout and lists | `grid`, `list`, `scroll`, `tabs`, `tab-bar`, `settings-list`, `content-panel`, `card`, `accordion`, `aspect-ratio`, `divider`, `item` |
151
161
  | 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
162
  | Feedback and data | `toast`, `admonition`, `loading`, `skeleton`, `error-boundary`, `badge`, `chip`, `kbd`, `code`, `link-preview`, `connection-status`, `connection-dots` |
153
163
  | Charts and motion | `composition-bar`, `dot-grid-meter`, `stat-bar`, `activity-heatmap`, `motion`, `animated-check`, `icon-circle`, `radio-indicator` |
@@ -157,7 +167,9 @@ Bloom publishes 88 subpath exports. Importing from the subpath rather than the r
157
167
 
158
168
  ## Documentation
159
169
 
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).
170
+ Component guides live in [`docs/`](./docs), one `.mdx` per family, and **they ship inside the package** at `node_modules/@oxyhq/bloom/docs/`. The copy you read there always describes the version you installed, which matters most for [migrating to 1.0](./docs/migrating-to-1.0.mdx).
171
+
172
+ Start with [getting started](./docs/getting-started.mdx), [theme](./docs/theme.mdx) and [design tokens](./docs/design-tokens.mdx). Then per surface: [dialog](./docs/dialog.mdx), [bottom sheet](./docs/bottom-sheet.mdx), [toast](./docs/toast.mdx), [alert](./docs/alert.mdx), [dropdown menu](./docs/dropdown-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).
161
173
 
162
174
  The theme reference is in [README.theme.md](./README.theme.md).
163
175
 
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Accordion
3
+ description: Collapsible sections — fully controlled, and when a disclosure is the wrong shape.
4
+ order: 28
5
+ ---
6
+
7
+ # Accordion
8
+
9
+ Sections that expand and collapse. Use it when a page holds more than a reader
10
+ needs at once and the sections are *independent* — an FAQ, a set of advanced
11
+ options, a long form split into stages.
12
+
13
+ ```tsx
14
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@oxyhq/bloom/accordion';
15
+
16
+ const [open, setOpen] = useState<string | string[] | undefined>('shipping');
17
+
18
+ <Accordion type="single" value={open} onValueChange={setOpen}>
19
+ <AccordionItem value="shipping">
20
+ <AccordionTrigger>When will it arrive?</AccordionTrigger>
21
+ <AccordionContent>Two to four working days.</AccordionContent>
22
+ </AccordionItem>
23
+ <AccordionItem value="returns">
24
+ <AccordionTrigger>Can I return it?</AccordionTrigger>
25
+ <AccordionContent>Within 30 days, unopened.</AccordionContent>
26
+ </AccordionItem>
27
+ </Accordion>
28
+ ```
29
+
30
+ ## Which prop decides what
31
+
32
+ | prop | decides |
33
+ | --------------- | -------------------------------------------------------------------- |
34
+ | `type` | `single` — one open section — or `multiple` |
35
+ | `value` | which section(s) are open: a string for `single`, an array for `multiple` |
36
+ | `onValueChange` | receives the next value, or `undefined` when everything closes |
37
+ | `disabled` | per `AccordionItem`, a section that cannot be opened |
38
+ | `icon` | per `AccordionTrigger`, replaces the default chevron |
39
+
40
+ **It is fully controlled — `value` and `onValueChange` are required.** There is
41
+ no uncontrolled mode and no `collapsible` flag: closing the last open section
42
+ means `onValueChange` hands you `undefined`, and whether you accept that or
43
+ re-open something is your decision, not the component's. That is what a wizard
44
+ (always something open) and an FAQ (nothing open is fine) actually differ by.
45
+
46
+ ```tsx
47
+ // FAQ: allow everything closed.
48
+ onValueChange={setOpen}
49
+
50
+ // Wizard: never let the current step close.
51
+ onValueChange={(next) => setOpen(next ?? open)}
52
+ ```
53
+
54
+ ## When an accordion is the wrong shape
55
+
56
+ - **The user needs to compare sections.** Collapsing hides exactly what they are
57
+ comparing.
58
+ - **The content is two lines.** More interaction than reading it would have been.
59
+ - **Something inside must be findable.** Browser find-in-page and screen-reader
60
+ search do not see collapsed content, so an accordion is a bad home for
61
+ anything a user might arrive looking for.
62
+
63
+ ## Animation
64
+
65
+ The height transition is driven imperatively from a measured content height
66
+ rather than by a layout animation. On web that distinction is load-bearing:
67
+ an animation started from a Reanimated mapper never ticks, so the obvious
68
+ "animate the content in" silently does nothing. If you nest something that
69
+ animates its own height, let one of the two own the measurement.
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: ActivityHeatmap
3
+ description: A calendar grid coloured by count — when something happened, over months.
4
+ order: 65
5
+ ---
6
+
7
+ # ActivityHeatmap
8
+
9
+ Days as cells, coloured by how much happened. Contributions, posts, sessions,
10
+ streaks.
11
+
12
+ ```tsx
13
+ import { ActivityHeatmap } from '@oxyhq/bloom/activity-heatmap';
14
+
15
+ <ActivityHeatmap
16
+ data={days}
17
+ numDays={182}
18
+ weekStartsOn={1}
19
+ onPressDay={(day) => showDay(day.date)}
20
+ />
21
+ ```
22
+
23
+ `data` is `{ date, count }[]`; `date` is an ISO day string.
24
+
25
+ ## Which prop decides what
26
+
27
+ | prop | decides |
28
+ | -------------------------- | --------------------------------------------------- |
29
+ | `data` | the counts |
30
+ | `numDays` / `endDate` | the window shown |
31
+ | `levels` | the count thresholds between colour steps |
32
+ | `colorScale` / `emptyColor`| the colours per level, and for zero |
33
+ | `weekStartsOn` | `0` Sunday or `1` Monday |
34
+ | `cellSize` / `gap` | the geometry |
35
+ | `monthLabels` / `weekdayLabels` | the axis text, for localisation |
36
+ | `onPressDay` | makes a cell pressable |
37
+
38
+ ## `levels` is the prop to think about
39
+
40
+ The default buckets suit a roughly uniform distribution. Real activity data is
41
+ usually skewed — a handful of enormous days and a long tail of ones — and with
42
+ default thresholds almost everything paints at the lowest level, so the chart
43
+ says "nothing much happened" about a year of work.
44
+
45
+ Set thresholds that match your distribution. Quantiles of the non-zero counts
46
+ are a better starting point than evenly spaced values.
47
+
48
+ Pass one more colour than you pass thresholds. A level is the number of
49
+ thresholds a count meets, so N thresholds address `colorScale[0..N]`; index 0 is
50
+ "positive, but below the first threshold". That index only exists while the
51
+ first threshold is 2 or more — start it at 1 and every positive count meets it,
52
+ so the faintest colour in your scale can never paint.
53
+
54
+ ## Zero is not the lowest level
55
+
56
+ `emptyColor` is separate from the scale on purpose. "Nothing happened" and "the
57
+ least that ever happens" are different facts, and collapsing them hides exactly
58
+ the streaks the chart is usually read for.
59
+
60
+ ## `weekStartsOn` is a locale decision
61
+
62
+ Not a style one. Getting it wrong shifts every column by a day, so the chart
63
+ disagrees with the user's calendar — which reads as bad data rather than as a
64
+ wrong setting. `monthLabels` and `weekdayLabels` exist for the same reason:
65
+ they must come from the app's locale, not from English defaults.
66
+
67
+ ## Give the window a reason
68
+
69
+ `numDays` should match a period the user thinks in — a year, six months, a
70
+ quarter. An arbitrary window invites the reader to work out what they are
71
+ looking at, and the label above the chart should say it outright.
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Admonition
3
+ description: A bordered callout inside the content — for information the reader must not scroll past.
4
+ order: 49
5
+ ---
6
+
7
+ # Admonition
8
+
9
+ A bordered block that interrupts the reading flow: a warning about a setting, a
10
+ note about a limitation, an apology for something that went wrong.
11
+
12
+ ```tsx
13
+ import { Admonition } from '@oxyhq/bloom/admonition';
14
+
15
+ <Admonition type="warning">
16
+ Turning this off signs you out of every other device.
17
+ </Admonition>
18
+ ```
19
+
20
+ For a callout with an action, compose the parts:
21
+
22
+ ```tsx
23
+ import {
24
+ AdmonitionRoot,
25
+ AdmonitionRow,
26
+ AdmonitionIcon,
27
+ AdmonitionContent,
28
+ AdmonitionText,
29
+ AdmonitionButton,
30
+ } from '@oxyhq/bloom/admonition';
31
+
32
+ <AdmonitionRoot type="error">
33
+ <AdmonitionRow>
34
+ <AdmonitionIcon />
35
+ <AdmonitionContent>
36
+ <AdmonitionText>We couldn't reach the server.</AdmonitionText>
37
+ <AdmonitionButton onPress={retry}>Try again</AdmonitionButton>
38
+ </AdmonitionContent>
39
+ </AdmonitionRow>
40
+ </AdmonitionRoot>
41
+ ```
42
+
43
+ ## `type` picks the icon and the border together
44
+
45
+ | `type` | for |
46
+ | --------- | ------------------------------------------------------- |
47
+ | `info` | a neutral note |
48
+ | `tip` | a suggestion — painted in the brand colour |
49
+ | `warning` | something that could go wrong if they continue |
50
+ | `error` | something that has gone wrong |
51
+ | `apology` | a limitation on our side, not the reader's mistake |
52
+
53
+ `AdmonitionIcon` reads the type from context, so the icon and the border cannot
54
+ disagree — which is how an `error` callout ends up with a warning icon when the
55
+ two are set by hand.
56
+
57
+ ## Admonition or toast
58
+
59
+ - **Admonition** — stays. It is *about* the content it sits next to, and the
60
+ reader can return to it.
61
+ - **[Toast](/docs/bloom/main/toast)** — passes. It is about something that just
62
+ happened.
63
+
64
+ Anything the user might need to read twice belongs in an admonition. A toast is
65
+ gone in four seconds and cannot be recovered.
66
+
67
+ ## `apology` exists because the wording differs
68
+
69
+ "Something went wrong" and "we couldn't do this, and it is our fault" are
70
+ different messages, and the second reads badly in an `error` box that looks
71
+ like the user broke something. Use it where the limitation is ours.
72
+
73
+ ## Do not stack them
74
+
75
+ Three callouts in a row is a page that has given up prioritising. If everything
76
+ is important, nothing reads as important — and a reader who learns to scroll
77
+ past one will scroll past the one that mattered.
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: AlertDialog
3
+ description: The declarative confirm dialog — and when to use `confirm()` instead.
4
+ order: 40
5
+ ---
6
+
7
+ # AlertDialog
8
+
9
+ A confirm dialog you render: a title, a description, and a confirm/cancel pair.
10
+
11
+ ```tsx
12
+ import { AlertDialog } from '@oxyhq/bloom/alert-dialog';
13
+
14
+ <AlertDialog
15
+ visible={confirmingDelete}
16
+ onClose={() => setConfirmingDelete(false)}
17
+ title="Delete this draft?"
18
+ description="This cannot be undone."
19
+ confirmLabel="Delete"
20
+ destructive
21
+ onConfirm={remove}
22
+ />
23
+ ```
24
+
25
+ ## Which prop decides what
26
+
27
+ | prop | decides |
28
+ | --------------- | ------------------------------------------------------------ |
29
+ | `visible` | whether it is open — the dialog is controlled |
30
+ | `onClose` | called whenever it closes, however it closed |
31
+ | `title` | the question |
32
+ | `description` | the consequence |
33
+ | `confirmLabel` / `cancelLabel` | the button text |
34
+ | `onConfirm` / `onCancel` | what each button does |
35
+ | `destructive` | paints the confirm button in the error role |
36
+ | `hideCancel` | one button only — an acknowledgement, not a choice |
37
+ | `dismissible` | whether a backdrop press or Escape closes it |
38
+
39
+ There is no free-form action array: the shape is confirm-and-cancel, and that
40
+ constraint is the point. A dialog with three or four actions is a menu wearing
41
+ a dialog's clothes, and the user has to read all of them before deciding.
42
+
43
+ ## AlertDialog or `confirm()`
44
+
45
+ Both ask the same question; they differ in where the answer goes.
46
+
47
+ - **`AlertDialog`** — declarative and controlled. Right when the open state is
48
+ already part of the screen's state.
49
+ - **[`confirm()`](/docs/bloom/main/surfaces)** — imperative, returns a promise.
50
+ Right inside an event handler or a mutation callback, where you want to
51
+ `await` the answer and there is no natural place to hang a `visible` flag.
52
+
53
+ The family owns no queue and no host: it bridges onto `Dialog`'s imperative
54
+ control internally, so it stacks with every other overlay and its exit
55
+ animation is not raced by a consumer unmounting in `onClose`.
56
+
57
+ ## `destructive` is a colour
58
+
59
+ It does not add a second confirmation and it does not make the action
60
+ reversible. If the action destroys something the user cannot get back, the
61
+ recovery path — an undo, a trash — is the real safeguard; the red button is
62
+ only a signal.
63
+
64
+ ## Write the question, not "Are you sure?"
65
+
66
+ The title says what will happen, and `confirmLabel` says what the button does —
67
+ "Delete" rather than "OK". Anyone dismissing dialogs quickly reads only the
68
+ button, and should still know what they agreed to.
69
+
70
+ ## `hideCancel` is for an acknowledgement
71
+
72
+ One button means there is no choice to make — something already happened and
73
+ the user is being told. Consider whether that needs a dialog at all: if nothing
74
+ is required of the reader, a [`toast`](/docs/bloom/main/toast) says it without
75
+ interrupting.
package/docs/alert.mdx ADDED
@@ -0,0 +1,93 @@
1
+ ---
2
+ title: alert()
3
+ description: Imperative one-shot confirmation dialogs, matching React Native's Alert.alert signature.
4
+ order: 6
5
+ ---
6
+
7
+ # alert()
8
+
9
+ Imperative one-shot confirmation dialogs, matching React Native's `Alert.alert(title, message?, buttons?)` signature. You can call `alert()` from anywhere — an event handler, an async callback, a top-level helper — without threading a provider through the call site.
10
+
11
+ It presents onto Bloom's shared **surface stack**, the same one `confirm()`, `prompt()` and `surfaces.present()` use, so an alert layers correctly above whatever is already open. Two `alert()` calls in a row STACK; they do not queue.
12
+
13
+ ## Setup
14
+
15
+ Mount the surface host once near your app root:
16
+
17
+ ```tsx
18
+ import { SurfaceProvider } from '@oxyhq/bloom';
19
+
20
+ <SurfaceProvider>
21
+ <App />
22
+ </SurfaceProvider>
23
+ ```
24
+
25
+ `<SurfaceHost />` on its own does the same job without wrapping `children`, if you'd rather place it yourself.
26
+
27
+ See [Getting started](/docs/bloom/main/getting-started) for the full provider stack.
28
+
29
+ ## Usage
30
+
31
+ ```tsx
32
+ import { alert } from '@oxyhq/bloom';
33
+
34
+ alert('Sign out?', 'Are you sure you want to sign out of this device?', [
35
+ { text: 'Cancel', style: 'cancel' },
36
+ { text: 'Sign out', style: 'destructive', onPress: doSignOut },
37
+ ]);
38
+
39
+ // Single OK button (default when no buttons passed):
40
+ alert('Saved');
41
+ ```
42
+
43
+ ## Signature
44
+
45
+ ```ts
46
+ function alert(
47
+ title: string,
48
+ message?: string,
49
+ buttons?: AlertButton[],
50
+ ): void
51
+ ```
52
+
53
+ ### `AlertButton`
54
+
55
+ | Prop | Type | Description |
56
+ |------|------|-------------|
57
+ | `text` | `string` | Required label. |
58
+ | `style?` | `'default' \| 'cancel' \| 'destructive'` | Defaults to `'default'`. |
59
+ | `onPress?` | `() => void` | Fires on the press, as the surface animates out. |
60
+
61
+ ## Patterns
62
+
63
+ ### Drop-in replacement for `Alert.alert`
64
+
65
+ If you're migrating from React Native's built-in `Alert`, you can swap the import:
66
+
67
+ ```diff
68
+ -import { Alert } from 'react-native';
69
+ +import { alert } from '@oxyhq/bloom';
70
+
71
+ -Alert.alert('Title', 'Message');
72
+ +alert('Title', 'Message');
73
+ ```
74
+
75
+ `alert()` matches the same call signature, so existing call sites work unchanged.
76
+
77
+ <Callout variant="warning" title="alert vs Dialog">
78
+ `alert()` is for **imperative one-shot prompts** (e.g. confirming destructive actions from a deeply nested callback). When you need state-driven control over a dialog (open/close timing, busy states), use the declarative [`<Dialog>`](/docs/bloom/main/dialog) component instead.
79
+ </Callout>
80
+
81
+ ## Related
82
+
83
+ `alert()` answers nothing. Its two siblings on the same stack do:
84
+
85
+ ```tsx
86
+ import { confirm, prompt } from '@oxyhq/bloom/surfaces';
87
+
88
+ const ok = await confirm({ title: 'Delete app?', destructive: true });
89
+ const name = await prompt({ title: 'Name this list' }); // string | null
90
+ ```
91
+
92
+ `confirm` resolves `false` and `prompt` resolves `null` on a backdrop / Escape /
93
+ Android-back dismissal, so neither can hang.
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: AnimatedCheck
3
+ description: A checkmark that draws itself — confirmation for something that just finished.
4
+ order: 67
5
+ ---
6
+
7
+ # AnimatedCheck
8
+
9
+ A checkmark that strokes itself in when told to.
10
+
11
+ ```tsx
12
+ import { AnimatedCheck, type AnimatedCheckRef } from '@oxyhq/bloom/animated-check';
13
+
14
+ const check = useRef<AnimatedCheckRef>(null);
15
+
16
+ <AnimatedCheck ref={check} size={48} />
17
+
18
+ // when the thing succeeds
19
+ check.current?.play();
20
+ ```
21
+
22
+ ## Which prop decides what
23
+
24
+ | prop | decides |
25
+ | ------- | ---------------------------- |
26
+ | `size` | the glyph size |
27
+ | `color` | the stroke colour |
28
+
29
+ The animation itself is imperative: `ref.play()`. There is no `visible` or
30
+ `animate` prop, because the interesting moment is a **transition** — the check
31
+ should draw when the operation finishes, not whenever the component happens to
32
+ mount or a prop happens to flip.
33
+
34
+ ## Where it belongs
35
+
36
+ The end of something the user waited for: a payment confirmed, a file uploaded,
37
+ a form submitted. It replaces the spinner that was there.
38
+
39
+ Not as a static tick in a list — that is an icon
40
+ (`Check_Stroke2_Corner0_Rounded`) and drawing it with an animation component
41
+ costs an animation and a ref for a glyph. And not for every save: a check
42
+ animation on an autosave is motion for something the user did not ask about.
43
+
44
+ ## Say it in words too
45
+
46
+ An animation is not announced. Whatever the check confirms should also be
47
+ readable — a line of text beside it, or a [toast](/docs/bloom/main/toast) — or a
48
+ screen reader user gets no confirmation at all.
49
+
50
+ ## Reduced motion
51
+
52
+ If the user has asked for reduced motion, show the check rather than drawing
53
+ it. The information is the checkmark; the stroke is decoration.
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: AspectRatio
3
+ description: A box that keeps a fixed width-to-height ratio.
4
+ order: 25
5
+ ---
6
+
7
+ # AspectRatio
8
+
9
+ A box that keeps a fixed width-to-height ratio while the layout around it decides its width. The usual job: a media well that must not jump as an image loads, because its height was known before the image was.
10
+
11
+ ```tsx
12
+ import { AspectRatio } from '@oxyhq/bloom/aspect-ratio';
13
+
14
+ <AspectRatio ratio={16 / 9}>
15
+ <Image source={{ uri: cover }} style={{ width: '100%', height: '100%' }} />
16
+ </AspectRatio>
17
+ ```
18
+
19
+ `ratio` is width divided by height and defaults to `1`. A number rather than a preset name, for the same reason shadcn's web `AspectRatio` takes one: a ratio is arithmetic the call site already has, and naming presets needs a new name for every ratio anyone wants.
20
+
21
+ ## A ratio it cannot use takes the default
22
+
23
+ `0`, `NaN`, `Infinity` and negatives are not ratios — a box has positive finite lengths on both axes — and every one of them comes out of the arithmetic above when the image has not loaded yet: `0 / 0` is `NaN`, `w / 0` is `Infinity`, `0 / h` is `0`. Passed through, they leave the well with no usable height, which is the layout jump this component exists to prevent. So they fall back to `1` rather than reaching the style.
24
+
25
+ You still want a real number here. The fallback keeps the well a well; it does not know your media's shape, so a component that can be told the ratio ahead of the load should be told it.
26
+
27
+ ## It fills its parent's width
28
+
29
+ An aspect box with no width is zero-sized, and "fill the parent, derive the height" is what every call site wants. So the width is `100%` and the height follows from `ratio`. A caller who needs a fixed width passes one through `style`, which lands last:
30
+
31
+ ```tsx
32
+ <AspectRatio ratio={1} style={{ width: 96, borderRadius: 12, overflow: 'hidden' }}>
33
+ <Avatar source={user.avatar} />
34
+ </AspectRatio>
35
+ ```
36
+
37
+ ## It takes no `className`
38
+
39
+ `aspectRatio` is the ONE property this component exists to set, and react-native-css merges utilities into `style` before inline style — so a caller writing `className="aspect-video"` would have it silently overwritten by the `ratio` prop, with no error anywhere.
40
+
41
+ One owner per property: use this component with `ratio`, or a plain `View` with the class, never both. Wrap it for the surrounding layout classes.
42
+
43
+ ## Not a port
44
+
45
+ react-native-reusables' `aspect-ratio.tsx` is four lines re-exporting `@rn-primitives/aspect-ratio`'s Root — a primitive that exists on the web because CSS needed `padding-bottom` tricks before `aspect-ratio` landed. React Native has had `aspectRatio` as an ordinary style property the whole time, so there was nothing to copy. What earns the name here is the shadcn spelling and the two decisions above.
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: AvatarGroup
3
+ description: The one stacked-avatar component. Apps must not keep a local avatar stack.
4
+ order: 22
5
+ ---
6
+
7
+ # AvatarGroup
8
+
9
+ Overlapping avatars with an overflow count — "3 people are typing", "12 members",
10
+ a facepile on a post. **Every grouped or stacked avatar UI in the ecosystem uses
11
+ this.** App-local avatar-stack components are not a variation to be tolerated;
12
+ they drift on overlap, ring colour and overflow rules, and they miss the avatar
13
+ resolution rules below.
14
+
15
+ ```tsx
16
+ import { AvatarGroup } from '@oxyhq/bloom/avatar-group';
17
+
18
+ <AvatarGroup
19
+ items={members}
20
+ max={5}
21
+ total={members.length}
22
+ size={32}
23
+ onPressItem={(item) => router.push(`/@${item.username}`)}
24
+ />
25
+ ```
26
+
27
+ ## Which prop decides what
28
+
29
+ | prop | decides |
30
+ | ------------------ | -------------------------------------------------------------- |
31
+ | `items` | who is shown, in order |
32
+ | `max` | how many faces before the overflow chip |
33
+ | `total` | the number the overflow chip counts to |
34
+ | `size` / `overlap` | the geometry |
35
+ | `ringColor` | the colour of the gap between overlapping faces |
36
+ | `onPressItem` | makes each face a link to that profile |
37
+ | `hoverCard` | a hover card on web; a no-op on native, where there is no hover |
38
+ | `renderItemAction` | a slot on each face |
39
+
40
+ `total` is separate from `items.length` on purpose: a list endpoint usually
41
+ returns a page of members plus a full count, and the overflow chip should say
42
+ `+248` rather than `+3`.
43
+
44
+ ## The avatar prop that goes wrong silently
45
+
46
+ Pass an Oxy avatar to `source`, **not** `uri`.
47
+
48
+ ```tsx
49
+ items={users.map((u) => ({ id: u.id, source: u.avatar, name: u.displayName }))} // ✅
50
+ items={users.map((u) => ({ id: u.id, uri: u.avatar }))} // ✗
51
+ ```
52
+
53
+ `source` sends a non-URL string — an Oxy file id — through the consumer's
54
+ registered `ImageResolver`. `uri` is taken as a raw URL, so a file id resolves
55
+ to nothing, the image 404s, and every face falls back to its initial. Nothing
56
+ throws. The symptom is "all avatars show a placeholder character", which reads
57
+ as missing data rather than as a wiring mistake.
58
+
59
+ ## `ringColor` is the surface behind, not a decoration
60
+
61
+ The gap between overlapping faces is drawn in `ringColor`, so it must match
62
+ whatever the group sits **on** — the page background on a screen, the `card`
63
+ role inside a card. A ring left at the default over a coloured surface is the
64
+ usual reason a facepile looks like it has white halos.
65
+
66
+ ## `hoverCard`
67
+
68
+ Web-only by construction: hover does not exist on touch, so the prop is a no-op
69
+ on native rather than a different interaction. The card itself is
70
+ [`UserHoverCard`](/docs/bloom/main/user-hover-card), which holds no follow
71
+ state — the consumer passes the SDK's `FollowButton` into its `action` slot.
72
+
73
+ `renderItemAction` is the only slot the hover card exposes per item.
74
+ `UserHoverCard`'s `footer` is not forwarded: a facepile hover card is a glance at
75
+ who these people are, and an app that wants a fuller preview renders
76
+ `UserHoverCard` behind its own trigger.
77
+
78
+ The card floats via `Portal`, imported from `'../portal/index.web'` rather than
79
+ the bare `'../portal'` — export conditions don't apply to relative specifiers,
80
+ so a bare import here resolves to the NATIVE barrel off Metro (Vite, webpack,
81
+ SSR), and the card renders nothing, silently, because the native `Portal`
82
+ needs a `PortalOutlet` no web app mounts. If you touch this import, verify
83
+ against the BUILT artefact, not just the source.
84
+
85
+ The card floats under a `View` with `onPointerEnter`/`onPointerLeave`, and that
86
+ must not become a `Pressable`. react-native-web's `Pressable` dispatches a
87
+ bubbling `react-gui:hover:lock` event on hover, and an ancestor `Pressable` that
88
+ is currently hovered ends its own hover for any lock it did not send — so a
89
+ `Pressable` bridge dismissed the card the moment the cursor reached the injected
90
+ FollowButton or the identity area. Nothing was logged; the card just went away
91
+ about 120ms later.