@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,434 @@
1
+ ---
2
+ title: Migrating to 1.0
3
+ description: Bloom 1.0.0 moves with @oxyhq/services 30.0.1 and @oxyhq/core 21.0.0 as one upgrade, plus every breaking change between 0.89.0 and 1.0.0, derived from the published tarballs rather than from commit messages.
4
+ order: 2
5
+ ---
6
+
7
+ # Migrating to 1.0
8
+
9
+ Bloom follows the clean-cut rule: there are no `@deprecated` aliases and no
10
+ old-name shims anywhere in this release. **This page is the only bridge.** If a
11
+ name below is gone, it is gone in the build you install — nothing re-exports it
12
+ under the old spelling.
13
+
14
+ <Callout variant="info" title="Which version are you on?">
15
+ `0.87.1 → 0.88.0 → 0.89.0` was **purely additive**: seven new symbols on
16
+ `@oxyhq/bloom/design-tokens` plus the `./design-tokens/tokens.json` subpath, and
17
+ nothing removed or renamed. So every change on this page applies in full whether
18
+ you are coming from 0.87.1, 0.88.0 or 0.89.0.
19
+ </Callout>
20
+
21
+ <Callout variant="warning" title="`^` means something different now">
22
+ Under a `0.x` version, `^0.89.0` resolves to `>=0.89.0 <0.90.0` — a caret pinned
23
+ you to one minor. From 1.0.0, `^1.0.0` resolves to `>=1.0.0 <2.0.0`, so a caret
24
+ now accepts every future minor. That is the intended semantics, but it is a
25
+ change in what your existing range does.
26
+ </Callout>
27
+
28
+ ## Before anything else: this is a three-package upgrade
29
+
30
+ Bloom 1.0.0 is not a package you can bump on its own. If your app also uses
31
+ `@oxyhq/services`, the move is **three packages in one commit**:
32
+
33
+ ```json
34
+ "@oxyhq/bloom": "^1.0.0",
35
+ "@oxyhq/services": "^30.0.1",
36
+ "@oxyhq/core": "^21.0.0"
37
+ ```
38
+
39
+ Not three steps. Both intermediate states are broken, and neither announces
40
+ itself: one cannot produce a warning at all, and the other produces exactly one
41
+ line, on one run.
42
+
43
+ ### Bloom alone breaks `@oxyhq/services`, silently
44
+
45
+ Every published `@oxyhq/services` below `30.0.0` imports two subpaths that 1.0.0
46
+ deleted, from its own source:
47
+
48
+ | File in `@oxyhq/services` | Imports |
49
+ | --- | --- |
50
+ | `src/ui/components/FollowTargetButton.tsx` | `@oxyhq/bloom/menu` |
51
+ | `src/ui/components/authChooser/TroubleDisclosure.tsx` | `@oxyhq/bloom/collapsible` |
52
+
53
+ Both are present in the published `29.0.0` tarball in `src/` **and** in both
54
+ compiled `lib/` trees, so it does not matter whether your bundler reads Bloom's
55
+ source or its build. Neither is in `30.0.1`. `FollowTargetButton` is the one that
56
+ usually surfaces first, because it is on the follow button most apps render.
57
+
58
+ Nothing warns you, and it is not that the warning is easy to miss — **no warning
59
+ is possible.** Every `@oxyhq/services` below 30 declares its Bloom peer as
60
+ `">=0.59.0"` (or looser), an unbounded range that `1.0.0` satisfies. There is no
61
+ mismatch for a package manager to report. `@oxyhq/bloom` declares no `@oxyhq/*`
62
+ peer of its own, so it cannot report one either. The failure appears at bundle
63
+ time as an unresolved module — on web, typically a white screen.
64
+
65
+ ### `@oxyhq/services` alone breaks against an old `@oxyhq/core`
66
+
67
+ `@oxyhq/services@30.0.1` names **12 `@oxyhq/core` symbols across 10 of its own
68
+ source files that do not exist in `@oxyhq/core@20.1.0`**: `DeviceContext`,
69
+ `SharedDeviceCredential`, `SharedDeviceCredentialRead`,
70
+ `SharedDeviceCredentialStore`, `SwitcherContextRow`, `SwitcherPrincipalRow`,
71
+ `buildSwitcherRows`, `createSharedMirroringAuthStateStore`,
72
+ `normalizeSharedDeviceSessionRead`, `projectDevicePrincipals`,
73
+ `resolveActiveContext` and `showsPrincipalHeaders`. All 144 core bindings
74
+ services imports exist in `21.0.0`.
75
+
76
+ This is not new in 30 — `@oxyhq/services@29.0.0` names the same 12. The core
77
+ floor moved to `^21.0.0` in services 29, so if you are skipping several majors
78
+ you are crossing it whichever version you land on.
79
+
80
+ Here the package manager does speak, but only just:
81
+
82
+ ```
83
+ warn: incorrect peer dependency "@oxyhq/core@20.1.0"
84
+ ```
85
+
86
+ One line, naming the version but not which package wanted what, on the run that
87
+ actually resolves. Re-run `bun install` against a lockfile that already
88
+ satisfies the tree and you get `Checked N installs ... (no changes)` and no
89
+ warning at all — which is the state most people are in by the time they look.
90
+
91
+ <Callout variant="warning" title="Do not install `@oxyhq/services@30.0.0`">
92
+ It is deprecated on npm: it published without `lib/`, so 26 of its 33 exports
93
+ targets are missing. `30.0.1` is the version.
94
+ </Callout>
95
+
96
+ ### Whether your typecheck can see any of this
97
+
98
+ `@oxyhq/services` publishes its main entry with a `react-native` condition that
99
+ is a **bare string** pointing at TypeScript source:
100
+
101
+ ```json
102
+ "exports": { ".": { "react-native": "./src/index.ts", "import": { … }, … } }
103
+ ```
104
+
105
+ So a project whose `customConditions` include `react-native` resolves
106
+ `@oxyhq/services` to `node_modules/@oxyhq/services/src/index.ts` and typechecks
107
+ the package's own source — the core mismatch lands as errors under
108
+ `node_modules/`, all naming `"@oxyhq/core"` (19 of them for `30.0.1` against
109
+ core `20.1.0`). A project without that condition resolves to
110
+ `lib/typescript/module/index.d.ts` instead, and with `skipLibCheck: true` — the
111
+ default in the Expo and Vite templates — **sees nothing at all: zero errors.**
112
+ Its build is green and its web bundle white-screens.
113
+
114
+ Which side you are on:
115
+
116
+ ```bash
117
+ # from the package directory, not the monorepo root, and name the tsconfig your
118
+ # typecheck script names — a references-only root tsconfig resolves nothing
119
+ bunx tsc -p tsconfig.json --showConfig | grep customConditions
120
+ ```
121
+
122
+ `["react-native"]` means your typecheck is a real check. No output means it is
123
+ not, and a green typecheck is not evidence about this upgrade. Expo apps get the
124
+ condition from `expo/tsconfig.base`; DOM/Vite packages generally do not.
125
+
126
+ If you are on the blind side and want one run that can see it, typecheck once
127
+ with `--skipLibCheck false`: services' shipped declarations reference the same
128
+ core symbols, so a stale core produces errors there too (fewer of them — 4 in
129
+ the case measured — but enough to fail the run).
130
+
131
+ ### Assert the version that resolved, not the one you declared
132
+
133
+ Two ways a "done" upgrade is not done:
134
+
135
+ - **A `packages/*` workspace glob does not match the repo root manifest.** A
136
+ version pinned in five places, four of them under `packages/`, edits cleanly,
137
+ and `bun install` answers `Checked 1539 installs (no changes)` — the root
138
+ manifest still pins the old major and `node_modules` never moved.
139
+ - **A caret range that a lockfile already satisfies does not re-resolve.**
140
+
141
+ Both are invisible if you read `package.json`. Read what resolved instead, from
142
+ each workspace package:
143
+
144
+ ```bash
145
+ bun pm ls | grep -E '@oxyhq/(bloom|services|core)@'
146
+ # or, from the package directory:
147
+ node -e 'for (const p of ["@oxyhq/bloom","@oxyhq/services","@oxyhq/core"]) console.log(p, require(p + "/package.json").version)'
148
+ ```
149
+
150
+ Expect `1.0.0`, `30.0.1` and `21.0.0`. Anything else means the edit did not land.
151
+
152
+ <Callout variant="info" title="How the numbers on this page were measured">
153
+ Everything above was read out of the published npm tarballs and registry
154
+ metadata for `@oxyhq/bloom@1.0.0`, `@oxyhq/services@29.0.0` and `30.0.1`, and
155
+ `@oxyhq/core@20.1.0` and `21.0.0`, on 2026-08-17. The symbol counts come from
156
+ an AST walk of services' `src/` cross-checked against each core version's
157
+ exported symbol set through the TypeScript checker; the error counts and the
158
+ warning text come from installing that pair into a scratch project and running
159
+ `tsc` and `bun install` against it. Version numbers move — re-read yours with
160
+ the commands above rather than trusting these.
161
+ </Callout>
162
+
163
+ ## How this list was derived
164
+
165
+ Not from commit subjects. The published `@oxyhq/bloom@0.87.1` and `0.89.0`
166
+ tarballs were unpacked and their `package.json#exports` compared against this
167
+ build's, and the exported symbol set of each of the 88 typed subpaths was read
168
+ out of `lib/typescript/` on both sides through the TypeScript checker, so a
169
+ re-export barrel is followed rather than guessed. Prop shapes were diffed the
170
+ same way, because a removed prop leaves `ButtonProps` present and its shape
171
+ changed.
172
+
173
+ One correction that matters if you re-run such a census yourself: a symbol moved
174
+ **into a namespace** looks exactly like a deleted symbol when you only read
175
+ top-level module exports. The fifteen colour-engine names below are in that
176
+ class — they are renamed, not removed.
177
+
178
+ ## Subpaths removed
179
+
180
+ Four subpaths no longer exist. An import of any of them is a hard resolution
181
+ failure, not a type error.
182
+
183
+ | Removed | Replacement | Why |
184
+ | --- | --- | --- |
185
+ | `@oxyhq/bloom/menu` | `@oxyhq/bloom/dropdown-menu` | The family was ported to shadcn/react-native-reusables' public API. Every `Menu*` export is `DropdownMenu*`. |
186
+ | `@oxyhq/bloom/collapsible` | `@oxyhq/bloom/accordion` | `Collapsible` called `UIManager.setLayoutAnimationEnabledExperimental(true)` at module scope and reached the root barrel through `export *`, so that side effect ran for every barrel consumer on Android old-arch whether or not the component was used. |
187
+ | `@oxyhq/bloom/grouped-buttons` | `@oxyhq/bloom/settings-list` + `@oxyhq/bloom/item` | The only one of the three row renderers with no `accessibilityRole`, no `accessibilityLabel` and no `aria-disabled`, built on `TouchableOpacity`/`activeOpacity` instead of `useInteractionState`. |
188
+ | `@oxyhq/bloom/pressable-with-hover` | none — inline it | One internal user. It now lives inside `zoomable-image-gallery` as `NavArrow`, the package's only hover-styled control. |
189
+
190
+ ### Subpaths added
191
+
192
+ `@oxyhq/bloom/dropdown-menu`, `@oxyhq/bloom/menubar`, `@oxyhq/bloom/radio`,
193
+ `@oxyhq/bloom/aspect-ratio`, and the `@oxyhq/bloom/design-tokens/tokens.json`
194
+ asset (the last already shipped in 0.89.0).
195
+
196
+ ## Renamed symbols
197
+
198
+ Mechanical renames — same component, new name.
199
+
200
+ | Old | New | Subpath |
201
+ | --- | --- | --- |
202
+ | `Menu`, `MenuTrigger`, `MenuContent`, `MenuItem`, `MenuGroup` | `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem`, `DropdownMenuGroup` | `./dropdown-menu` |
203
+ | `MenuDivider`, `ContextMenuDivider` | `DropdownMenuSeparator`, `ContextMenuSeparator` | `./dropdown-menu`, `./context-menu` |
204
+ | `useMenuControl`, `useMenuContext`, `MenuControlProps` | declared by `dropdown-menu` itself, no longer re-exported from `dialog` under an alias | `./dropdown-menu` |
205
+ | `Addon` | `InputGroupAddon` | `./input-group` |
206
+ | `Provider`, `Outlet` | `PortalProvider`, `PortalOutlet` | `./portal` |
207
+ | `Attachment` | `PromptInputAttachment` | `./prompt-input` |
208
+ | `zIndex` | `Z_INDEX` | `./styles` |
209
+ | `ChipVariant`, `BadgeVariant` | `AccentFill` | `./theme` |
210
+ | `ChipColor`, `BadgeColor` | `AccentTone` | `./theme` |
211
+
212
+ `InputGroup.Addon`, the static alias, is gone too — `InputGroupAddon` is the one
213
+ spelling. No Bloom compound carries a static alias.
214
+
215
+ For `Chip` and `Badge` the accepted **values** are almost unchanged; only the
216
+ type names moved. `BadgeVariant` was already `'solid' | 'subtle' | 'outlined'`,
217
+ which is exactly `AccentFill`, and `BadgeColor` is exactly `AccentTone`. The one
218
+ value that changed is Chip's `'soft'`, which is now `'subtle'`; `AccentTone` also
219
+ adds `'info'` for Chip, which previously had it on `Badge` only.
220
+
221
+ ### The colour engine is one namespace
222
+
223
+ Fifteen flat exports of `@oxyhq/bloom/theme` are now members of the `ColorEngine`
224
+ namespace, still published from the same subpath:
225
+
226
+ ```ts
227
+ // before
228
+ import { argbFromHex, generateRoleColors, quantizeImage } from '@oxyhq/bloom/theme';
229
+
230
+ // after
231
+ import { ColorEngine } from '@oxyhq/bloom/theme';
232
+ ColorEngine.argbFromHex(...);
233
+ ```
234
+
235
+ Affected: `argbFromHex`, `hexFromArgb`, `argbFromRgb`, `redFromArgb`,
236
+ `greenFromArgb`, `blueFromArgb`, `quantizeImage`, `seedsFromImagePixels`,
237
+ `seedHexFromImagePixels`, `generateRoleColors`, and the types `RoleName`,
238
+ `RoleColors`, `GenerateOptions`, `SchemeVariant`, `AccentSources`.
239
+
240
+ `useTheme`, `BloomThemeProvider` and the tokens stay flat — those are used
241
+ fleet-wide and did not move.
242
+
243
+ ### Theme symbols that moved subpath
244
+
245
+ `getPresetVars`, `buildSeedScopeVars`, `SeedScopeOptions` and `ExplicitAccents`
246
+ were reachable from two subpaths. They now live only in the react-free barrel a
247
+ build script can import:
248
+
249
+ ```ts
250
+ -import { getPresetVars } from '@oxyhq/bloom/theme';
251
+ +import { getPresetVars } from '@oxyhq/bloom/design-tokens';
252
+ ```
253
+
254
+ `getPresetVars` and `ExplicitAccents` are also on `@oxyhq/bloom/preset-vars`.
255
+
256
+ ### `alert()` and `confirm()` moved to the surface stack
257
+
258
+ `alert()` lived on `./dialog`, `confirm()` on `./alert-dialog`. Both are now on
259
+ `@oxyhq/bloom/surfaces`, along with `AlertButton` and `AlertButtonStyle`.
260
+
261
+ ```ts
262
+ -import { alert } from '@oxyhq/bloom/dialog';
263
+ -import { confirm } from '@oxyhq/bloom/alert-dialog';
264
+ +import { alert, confirm } from '@oxyhq/bloom/surfaces';
265
+ ```
266
+
267
+ Their hosts — `BloomDialogProvider` and `AlertDialogHost` — are **deleted**, not
268
+ renamed. Both were module-scope FIFO queues with a host component each; remove
269
+ them from your provider tree and mount the single `<SurfaceHost>` instead. A
270
+ one-at-a-time queue orders by arrival into its own queue, so it could not layer
271
+ over a surface it did not know about. Consequence: **two `alert()` calls in a row
272
+ now stack rather than queue.**
273
+
274
+ `SurfaceConfirmOptions.message` and `SurfacePromptOptions.message` are now
275
+ `description`, matching `DialogProps`.
276
+
277
+ ## Deleted with no replacement
278
+
279
+ | Symbol | Was on | Note |
280
+ | --- | --- | --- |
281
+ | `BloomDialogProvider` | `./dialog` | see above |
282
+ | `AlertDialogHost`, `ConfirmOptions` | `./alert-dialog` | see above |
283
+ | `TooltipSheetCompatProvider` | `./tooltip` | `return <>{children}</>` on both platforms, zero importers — an unfinished port, not a redundant shim |
284
+ | `SkeletonLoadingProps` | `./loading` | `Loading variant="skeleton"` drew static Views under the same name as the `skeleton` family, which has a real shimmer loop |
285
+ | `useSharedInputStyles` | `./text-field` | module-private; was never usable |
286
+ | `useIsFullscreen` | `./prompt-input` | zero callers |
287
+ | `DIALOG_SHEET_BACKDROP_TESTID` | `./dialog` | off the public surface |
288
+ | `usePopoverControl`, `usePopoverContext`, `PopoverControlProps`, `PopoverPlacement`, `PopoverTriggerRenderProps`, `PopoverTriggerState` | `./popover` | Popover took the Radix shape (`open`/`defaultOpen`/`onOpenChange`, `asChild`, `side`/`align`/`sideOffset`); the render-prop trigger is gone |
289
+ | `MenuItemText`, `MenuItemIcon`, `ContextMenuItemText`, `ContextMenuItemIcon` | `./menu`, `./context-menu` | The four menu families share one row vocabulary now. A row is `DropdownMenuItem` / `ContextMenuItem` with its label as children; use `DropdownMenuShortcut` for trailing text and `DropdownMenuLabel` for a group heading. There is no `*ItemText` or `*ItemIcon` in 1.0.0. |
290
+
291
+ ## Prop changes
292
+
293
+ | Component | Change |
294
+ | --- | --- |
295
+ | `Button` | `activeOpacity` removed. Press feedback is the library's decision, not a per-call knob. |
296
+ | `Loading` | `variant` no longer accepts `"skeleton"`. Use the `skeleton` family. |
297
+ | `Chip` | `variant="soft"` → `variant="subtle"`, one spelling matching the token family. `color="info"` now exists (it was on `Badge` only). The `onClose` affordance draws `TimesLarge_Stroke2_Corner0_Rounded`, not the `×` glyph. |
298
+ | `Badge` | A `dot` Badge paints the tone's fill whatever the variant — `dot variant="outlined"` used to be a fully transparent circle. |
299
+ | `Popover` | `PopoverContentProps.offset` and `.placement` removed (use `sideOffset` and `side`/`align`); `PopoverProps.control` removed. |
300
+ | `TextField` | `TextFieldIcon` and `TextFieldSuffix` now **throw** outside a `<TextField>`. `TextFieldInput` and `TextFieldLabel` deliberately still work standalone. |
301
+
302
+ ## Behavioural changes with no API change
303
+
304
+ These break nothing at compile time and change what you see.
305
+
306
+ - **`Button` heights are now exactly 32 / 36 / 44** (small / medium / large) on
307
+ both platforms and in every variant. Hit slop brings the two smaller sizes up
308
+ to the 44dp touch-target floor.
309
+ - **`Button`'s `primary` and `destructive` fills are a glass material** at fill
310
+ alpha 0.85. `inverse` stays opaque. Full AA cost table in
311
+ [Button](/docs/bloom/main/button).
312
+ - **`AspectRatio` rejects a degenerate ratio** — a `ratio` that is zero,
313
+ negative or non-finite falls back to the default instead of collapsing the
314
+ box.
315
+ - **`ActivityHeatmap`'s default palette declares only the steps it can paint.**
316
+ `DEFAULT_ALPHAS` carried five steps but the first could never be reached.
317
+ Rendered with the default props this is byte-identical to what shipped; if you
318
+ passed your own `levels` **and** relied on the default alphas, re-check the
319
+ pairing.
320
+ - **`AvatarGroup` resolves identity as `displayName?.trim() || handle`.** It used
321
+ `??`, which falls through on null/undefined only, so a whitespace-only
322
+ `displayName` — which the API does send — beat the handle behind it and showed
323
+ the default avatar image.
324
+ - **Popover positions through the shared placement engine.** Its own resolver
325
+ clamped only the top, so a panel that fit neither above nor below its trigger
326
+ ran off the bottom of the viewport.
327
+ - **`Switch`, `Slider` and `DotGridMeter` warn once, in dev only, when they
328
+ mount with no accessible name.** All three draw no text, so nothing in the
329
+ component can supply one. `Switch` and `DotGridMeter` gained an optional
330
+ `accessibilityLabel` prop for it; the warning never fires in production, and
331
+ an empty or whitespace-only name counts as missing, because `aria-label=""`
332
+ names nothing.
333
+ - **The neutral surface ramp was respaced** so adjacent surfaces are actually
334
+ distinguishable in every preset — M3's 2-tone step measured below the
335
+ just-noticeable difference here. If you pinned Bloom token values in your own
336
+ fixtures or screenshots, expect them to move.
337
+ - **`AvatarGroup`'s hover card renders off Metro.** Its web fork named
338
+ `'../portal'` rather than `'../portal/index.web'`, and export conditions do
339
+ not apply to relative specifiers, so Vite/webpack/SSR got the native `Portal`
340
+ — which needs a `PortalOutlet` a web app never mounts — and the card drew
341
+ nothing.
342
+ - **`PromptInput` and `BloomColorScope`/`BloomSeedScope` now work off Metro.**
343
+ Both shipped `.web` forks with no `browser` condition, so Vite/webpack/SSR
344
+ resolved the native build: `onImagePaste` was missing entirely and the colour
345
+ scopes emitted no CSS custom properties at all.
346
+
347
+ ## Structural changes that are not API changes
348
+
349
+ Every family is now `index.ts` (a pure barrel) + `<Pascal>.tsx` + `types.ts`,
350
+ hooks are `use-kebab-case.ts`, and prop types live in `types.ts`. **The export
351
+ surface is unchanged by that refactor** — measured symbol-for-symbol across all
352
+ barrels, lost=0 gained=0. You only notice if you deep-import a file path inside
353
+ the package, which is not a supported entry point.
354
+
355
+ ## `jest.mock` call sites
356
+
357
+ A renamed subpath breaks `jest.mock('@oxyhq/bloom/<sub>')` silently — the string
358
+ is not an import, so no type checker and no import census sees it.
359
+
360
+ Measured across the Oxy fleet: **131 `jest.mock('@oxyhq/bloom/…')` call sites in
361
+ 48 files, referencing 20 distinct subpaths — all 20 still exist in 1.0.0.** No
362
+ mock needs to change. The subpaths mocked are `.`, `./avatar`, `./avatar-group`,
363
+ `./bottom-sheet`, `./button`, `./dialog`, `./hooks`, `./icons`, `./item`,
364
+ `./loading`, `./pressable-scale`, `./search`, `./settings-list`, `./skeleton`,
365
+ `./subtle-hover`, `./switch`, `./tab-bar`, `./text-field`, `./theme`, `./toast`.
366
+
367
+ If you add a mock for `./menu`, `./collapsible`, `./grouped-buttons` or
368
+ `./pressable-with-hover`, it will now silently mock a module that does not exist.
369
+
370
+ ## Measured impact across the Oxy fleet
371
+
372
+ <Callout variant="warning" title="This section is a scan of local checkouts, not of what you have installed">
373
+ The counts below come from scanning 15,544 tracked source files in the working
374
+ copies of every repo under `~/Oxy` on one machine. A working copy is whatever
375
+ branch someone left it on, which is not the same as any published version — the
376
+ `OxyHQServices` checkout scanned here sat on a topic branch pinned to
377
+ `@oxyhq/services@26.0.1`, four majors behind the published `30.0.1`, and that
378
+ one stale tree is why an earlier revision of this page named only one of the two
379
+ files services imports from a removed subpath. Treat this as a worked example of
380
+ what an upgrade turns up, not as an inventory of your repo. Scan your own:
381
+
382
+ ```bash
383
+ git grep -nE "@oxyhq/bloom/(menu|collapsible|grouped-buttons|pressable-with-hover)['\"]"
384
+ ```
385
+ </Callout>
386
+
387
+ As scanned, excluding Bloom's own tree: **68 call sites in 51 files across 15
388
+ repos need an edit** — Alia, Astro, CRM, Clarity, CrowdSource, Homiio, Inbox,
389
+ Mention, Mercaria, Moovo, Noted, OxyHQServices, Station, Syra and website. Four
390
+ of the 68 were imports of a deleted subpath and the other 64 were named
391
+ bindings, so 68 is a floor: the scan missed `FollowTargetButton.tsx` below,
392
+ which does not exist on the branch the `OxyHQServices` checkout was parked on.
393
+
394
+ Imports of a removed subpath fail to resolve. They fall into two groups, and the
395
+ fix is different for each.
396
+
397
+ **In `@oxyhq/services`' own code — fixed by upgrading, not by editing.** Read
398
+ out of the published `29.0.0` tarball rather than from a checkout, so it holds
399
+ for anyone below 30 regardless of what any working copy says:
400
+
401
+ | File in `@oxyhq/services` (any version below 30) | Subpath |
402
+ | --- | --- |
403
+ | `src/ui/components/FollowTargetButton.tsx` | `./menu` |
404
+ | `src/ui/components/authChooser/TroubleDisclosure.tsx` | `./collapsible` |
405
+
406
+ **In app code — you edit these.** Present on `origin/main` of each repo as of
407
+ 2026-08-17:
408
+
409
+ | File | Subpath |
410
+ | --- | --- |
411
+ | `Inbox/packages/frontend/components/settings/sections/AccountSection.tsx` | `./grouped-buttons` |
412
+ | `Homiio/packages/frontend/app/host/calendar.tsx` | `./menu` |
413
+
414
+ Named bindings that no longer exist, by symbol:
415
+
416
+ | Symbol | Sites | Fix |
417
+ | --- | --- | --- |
418
+ | `getPresetVars` | 10 | import from `@oxyhq/bloom/design-tokens` |
419
+ | `confirm` (from `./alert-dialog`) | 12 | import from `@oxyhq/bloom/surfaces` |
420
+ | `Provider` / `Outlet` (from `./portal`) | 6 + 6 | `PortalProvider` / `PortalOutlet` |
421
+ | `generateRoleColors` | 6 | `ColorEngine.generateRoleColors` |
422
+ | `BadgeColor` | 5 | `AccentTone` |
423
+ | `alert` (from `./dialog`) | 3 | import from `@oxyhq/bloom/surfaces` |
424
+ | `ChipColor`, `ChipVariant` | 2 + 2 | `AccentTone`, `AccentFill` |
425
+ | `BadgeVariant` | 1 | `AccentFill` |
426
+ | `BloomDialogProvider` | 2 | delete; mount `<SurfaceHost>` |
427
+ | `AlertDialogHost` | 1 | delete; mount `<SurfaceHost>` |
428
+ | `Menu*` + `useMenuControl` (one file, `Syra/packages/frontend/components/MediaCard.tsx`) | 7 | `DropdownMenu*` |
429
+ | `Collapsible` (`Astro/webui/.../collapsible-group.tsx`) | 1 | `Accordion` |
430
+
431
+ `@oxyhq/bloom/icons` is byte-identical between 0.87.1 and 1.0.0 — a census will
432
+ appear to flag three icon names in Bloom's own
433
+ `src/__tests__/icon-references.test.ts`, but those are that suite's own fixture
434
+ strings, not call sites.
@@ -0,0 +1,83 @@
1
+ ---
2
+ title: Motion
3
+ description: Shared durations and easings — and the three web failure modes that make animation code lie.
4
+ order: 57
5
+ ---
6
+
7
+ # Motion
8
+
9
+ Shared enter/exit presets and a screen transition, so a sheet and a menu on the
10
+ same screen do not open at subtly different speeds.
11
+
12
+ ```tsx
13
+ import {
14
+ ScaleAndFadeIn,
15
+ ScaleAndFadeOut,
16
+ ShrinkAndPop,
17
+ ScreenTransition,
18
+ } from '@oxyhq/bloom/motion';
19
+
20
+ <Animated.View entering={ScaleAndFadeIn} exiting={ScaleAndFadeOut}>
21
+ <Panel />
22
+ </Animated.View>
23
+ ```
24
+
25
+ The value of shared presets is not the numbers — it is that a change to "how
26
+ fast things feel" happens in one place rather than being renegotiated per
27
+ component. It is also where the web rules below are already applied, which is
28
+ the stronger reason to use them rather than hand-writing an enter.
29
+
30
+ ## Three ways animation silently does nothing on web
31
+
32
+ These are not Bloom quirks; they are how Reanimated behaves in the
33
+ configuration every Oxy web app ships. Each fails with **no console output**.
34
+
35
+ ### An animation started from a mapper never ticks
36
+
37
+ Returning `withTiming` / `withRepeat` from `useDerivedValue` or
38
+ `useAnimatedStyle` does nothing on web. Drive a shared value **imperatively**;
39
+ `useAnimatedStyle` only ever READS.
40
+
41
+ ```tsx
42
+ // ✅ someone calls this
43
+ progress.value = withTiming(1, { duration: 200 });
44
+
45
+ const style = useAnimatedStyle(() => ({ opacity: progress.value }), [progress]);
46
+ ```
47
+
48
+ ### Without the worklets Babel plugin, mappers re-run on their DEPS ARRAY
49
+
50
+ Not on auto-tracked reads. Omit an animating shared value from the deps and the
51
+ mapper runs once and freezes at frame 1. **List every shared value the mapper
52
+ reads** — note the `[progress]` above, which is doing real work.
53
+
54
+ `useAnimatedReaction` does not fix this: key it on a changing prop, not on a
55
+ shared value.
56
+
57
+ ### A custom `Keyframe` as `entering` freezes the element
58
+
59
+ An animation name absent from Reanimated's built-in map also schedules a
60
+ cleanup that pins the element — `position: absolute` plus a frozen box — at
61
+ `duration × 5`. It is a layout bug with no error, and `exiting` is unaffected,
62
+ so it looks intermittent.
63
+
64
+ ## Bloom's rule: pick the mechanism per DIRECTION
65
+
66
+ - **`entering`** runs on the real element. It is EITHER a predefined builder
67
+ (`FadeIn`, `SlideInUp`) OR driven imperatively from a shared value. Never a
68
+ `Keyframe`, never a custom builder.
69
+ - **`exiting`** runs on a throwaway clone, so a custom `Keyframe` is safe there —
70
+ and is the only way to express a multi-property, multi-stop shape.
71
+
72
+ Consequences to accept rather than work around: a predefined builder cannot
73
+ combine fade with scale, so a web enter drops one; and `Keyframe` has no
74
+ `.easing()`, so web keyframes run linear unless a per-stop easing happens to
75
+ resolve to one of Reanimated's built-in names. Add intermediate stops if the
76
+ curve matters.
77
+
78
+ ## Verifying
79
+
80
+ Jest and `tsc` catch none of these. Verify in a real **foregrounded** tab — a
81
+ hidden tab freezes plain CSS transitions too, which mimics "the class didn't
82
+ apply". Read the class attribute, or set `transitionProperty: 'none'` before
83
+ reading computed styles.
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Overlay
3
+ description: How a portaled surface takes its place in the stack — and the pointer-events rule that fails totally and silently.
4
+ order: 37
5
+ ---
6
+
7
+ # Overlay
8
+
9
+ `overlay/` exports `OverlayRoot` and `Backdrop`: the one way a portaled surface
10
+ establishes its interactive root, its position in the stack, and its
11
+ press-to-dismiss dim. Do not hand-roll either.
12
+
13
+ ```tsx
14
+ import { OverlayRoot, Backdrop } from '@oxyhq/bloom/overlay';
15
+
16
+ {open ? (
17
+ <Portal>
18
+ <OverlayRoot>
19
+ <Backdrop onPress={close} progress={progress} />
20
+ <Panel />
21
+ </OverlayRoot>
22
+ </Portal>
23
+ ) : null}
24
+ ```
25
+
26
+ ## A surface opened LATER paints above one opened earlier
27
+
28
+ That is decided once, in `overlay/stack.ts`, and applied by `OverlayRoot`.
29
+ **Never give an overlay surface a `zIndex` of its own.**
30
+
31
+ A per-component constant answers "which is on top" by what a surface *is*
32
+ rather than by when it was opened, so some pairings end up permanently
33
+ inverted — a menu that can never appear above a dialog, whatever order the user
34
+ opened them in.
35
+
36
+ To add a surface: render its portal body inside `<OverlayRoot>`, and put that
37
+ `OverlayRoot` **inside the guard that makes the surface appear**. The rank is
38
+ taken on mount, so the depth is right on the first paint. Order the parts
39
+ *within* one surface using `useOverlayLayerContext()`.
40
+
41
+ Toasts are the one deliberate exception: `ToastHost` mounts for the app's whole
42
+ life, so taking a rank would wedge the counter, and it passes an explicit
43
+ `zIndex` instead.
44
+
45
+ ## The `pointerEvents` rule
46
+
47
+ `box-none` and `box-only` are React Native-only values and **must be passed as
48
+ a prop, never inside a `style` object**.
49
+
50
+ ```tsx
51
+ <View pointerEvents="box-none" style={styles.root} /> // ✅
52
+ <View style={[styles.root, { pointerEvents: 'box-none' }]} /> // ✗ silently dropped
53
+ ```
54
+
55
+ react-native-web resolves those two values from the prop path only; as a style
56
+ entry they are dropped with no warning. `'auto'` and `'none'` *do* survive as
57
+ styles, which is exactly what makes the mistake easy to make and hard to see.
58
+
59
+ Why it is catastrophic: the web `Portal` root is `pointer-events: none`, and the
60
+ property **inherits**. A dropped opt-in makes the entire surface click-through —
61
+ backdrops do not dismiss, buttons do not press, clicks land on the app behind.
62
+ Escape still works, so it presents as a dismissal bug rather than as a
63
+ pointer-events bug. Outside a portal the same mistake inverts, and a full-bleed
64
+ band silently eats presses.
65
+
66
+ Verify dismissal in a real browser. Jest cannot see inheritance through a
67
+ portal, and the markup is valid either way.
68
+
69
+ ## Not everything overlay-shaped is in the stack
70
+
71
+ The **web** tooltip is not portaled and has no rank; the native one is and does.
72
+ Check for a `Portal` before assuming a surface participates.
73
+
74
+ On native there is a second mechanism z-index cannot reach: each of these
75
+ surfaces is its own RN `<Modal>` window.
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Popover
3
+ description: Arbitrary content anchored to a trigger — a sheet on native, a panel on web.
4
+ order: 21
5
+ ---
6
+
7
+ # Popover
8
+
9
+ A small surface of arbitrary content, anchored to what opened it. An anchored panel on web, a bottom sheet on native.
10
+
11
+ Three parts, shadcn's own: `Popover`, `PopoverTrigger`, `PopoverContent`.
12
+
13
+ Reach for it when the content is not a list of actions. When it IS a list of actions, reach for [DropdownMenu](/docs/bloom/main/dropdown-menu) — it gives you the rows, the keyboard state and the ARIA roles for free.
14
+
15
+ ## Basic
16
+
17
+ ```tsx
18
+ import { Popover, PopoverContent, PopoverTrigger } from '@oxyhq/bloom/popover';
19
+ import { Button } from '@oxyhq/bloom/button';
20
+ import { P } from '@oxyhq/bloom/typography';
21
+
22
+ <Popover>
23
+ <PopoverTrigger asChild label="What is this?">
24
+ <Button variant="secondary">What is this?</Button>
25
+ </PopoverTrigger>
26
+
27
+ <PopoverContent label="Two-factor authentication">
28
+ <View style={{ padding: 16, gap: 8 }}>
29
+ <P>A second step when you sign in, so a leaked password is not enough on its own.</P>
30
+ </View>
31
+ </PopoverContent>
32
+ </Popover>
33
+ ```
34
+
35
+ ## The panel has no padding of its own
36
+
37
+ Deliberate, and worth knowing before you wonder where the inset went. A popover's body is as often a list of `Item` rows (the combobox, the dialog header's overflow) as it is prose, and rows have to reach the panel edge to show their full-width highlight. So the panel keeps only the 4px vertical rhythm every anchored Bloom surface has, and a prose popover pads its own body — as the example above does.
38
+
39
+ ## Controlled
40
+
41
+ ```tsx
42
+ const [open, setOpen] = useState(false);
43
+
44
+ <Popover open={open} onOpenChange={setOpen}>
45
+ ```
46
+
47
+ Uncontrolled by default. `onOpenChange` fires either way. Closing it from inside its own content is a `setOpen(false)`.
48
+
49
+ ## Placement (web)
50
+
51
+ `PopoverContent` takes `side` (`'top' | 'bottom'`, default `'bottom'`), `align` (`'start' | 'center' | 'end'`, default `'center'`), `sideOffset`, `alignOffset`, `minWidth` and `maxWidth`. The surface flips to the other side when the named one does not fit and clamps into the viewport when neither does.
52
+
53
+ A popover centres on its trigger by default, where a menu starts at its left edge — that is the one placement default the two families disagree on, and it matches shadcn.
54
+
55
+ ## Shape
56
+
57
+ shadcn's popover is `w-72 p-4`: a fixed 288px card with a 16px inset, not a shrink-wrap around its content. Bloom applies both, first in the style array, so a body that is a list of rows rather than prose overrides them from `style` — the same thing a shadcn call site says with `className="w-auto p-0"`:
58
+
59
+ ```tsx
60
+ <PopoverContent label="Actions" style={{ width: 'auto', padding: 4 }}>
61
+ <Item title="Profile" onPress={openProfile} />
62
+ </PopoverContent>
63
+ ```
64
+
65
+ Rows have to reach the panel's edge to draw a full-width highlight, which is why `Combobox` and the dialog header's overflow menu both do this.
66
+
67
+ On native these are ignored; the sheet is anchored to the screen.
68
+
69
+ ## Dismissal
70
+
71
+ Press outside or hit Escape. `dismissible={false}` turns both off, for a popover the user has to answer.
72
+
73
+ Escape stops at the innermost open surface, so a popover opened from inside a dialog closes the popover and leaves the dialog up.