@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,66 @@
1
+ ---
2
+ title: DotGridMeter
3
+ description: How many out of a total, when the total is small enough to count.
4
+ order: 64
5
+ ---
6
+
7
+ # DotGridMeter
8
+
9
+ A grid of dots, some filled. It answers *how many out of how many* for a total
10
+ the eye can actually count.
11
+
12
+ ```tsx
13
+ import { DotGridMeter } from '@oxyhq/bloom/dot-grid-meter';
14
+
15
+ <DotGridMeter filled={34} total={50} columns={10} accessibilityLabel="Tokens used" />
16
+ ```
17
+
18
+ ## Which prop decides what
19
+
20
+ | prop | decides |
21
+ | -------------------------- | ---------------------------------------------- |
22
+ | `filled` / `total` | the count and the ceiling |
23
+ | `columns` | the grid width, which sets the row count |
24
+ | `dotSize` / `gap` | the geometry |
25
+ | `filledColor` / `emptyColor`| the two states |
26
+ | `accessibilityLabel` | what is being counted — see below |
27
+
28
+ ## Use it when the total is countable
29
+
30
+ Thirty-four of fifty tokens, seven of ten onboarding steps, twelve of twenty
31
+ invites. The point is that a reader can verify the number by looking — that is
32
+ what a dot grid gives you over a bar.
33
+
34
+ Over a few hundred it stops being countable and becomes a texture. Past that a
35
+ [`StatBar`](/docs/bloom/main/stat-bar) is the honest widget: it does not
36
+ pretend the reader can count.
37
+
38
+ ## `columns` decides the shape
39
+
40
+ Pick a value that makes the rows read as groups — 10 across for a total of 50
41
+ gives five clean rows of ten, which is countable at a glance. An awkward number
42
+ leaves a ragged last row and makes the total harder to read, not easier.
43
+
44
+ ## Both colours matter
45
+
46
+ The empty dots carry the *total*, so they must be visible. An `emptyColor` too
47
+ close to the background turns the widget into "34 dots" with no denominator,
48
+ which is the one thing it exists to show.
49
+
50
+ ## State the numbers as well
51
+
52
+ `34 / 50` in text beside the grid. The dots are the shape of the answer; the
53
+ text is the answer.
54
+
55
+ ## Name what is being counted
56
+
57
+ The grid announces itself as a `progressbar` carrying `aria-valuenow` /
58
+ `aria-valuemin` / `aria-valuemax`, so a screen reader reads the numbers — but
59
+ `progressbar` takes its name from the author alone. ARIA never computes one
60
+ from an element's contents for this role, and the contents are dots in any
61
+ case, so without `accessibilityLabel` the meter announces "34 of 50" against
62
+ nothing. `StatBar` gets this for free from its required `label`; a bare grid has
63
+ no such text.
64
+
65
+ Omitting it warns once in development, dev-only and never throwing. An empty or
66
+ whitespace-only string counts as missing.
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: DropdownMenu
3
+ description: An action menu anchored to a trigger — a bottom sheet on native, a dropdown on web.
4
+ order: 14
5
+ ---
6
+
7
+ # DropdownMenu
8
+
9
+ The menu that drops from a button. A bottom sheet on native, an anchored panel on web, one API on both.
10
+
11
+ The parts are shadcn's, so a `@shadcn/ui` or `react-native-reusables` call site ports across unchanged: `DropdownMenu` owns the open state, `DropdownMenuTrigger` opens it, `DropdownMenuContent` is the surface, and the rows are `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup` / `DropdownMenuRadioItem`, `DropdownMenuLabel`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuGroup` and the `DropdownMenuSub` trio.
12
+
13
+ `ContextMenu` and `Menubar` publish the same row vocabulary under their own prefixes. They are one implementation, so a row behaves identically in all three.
14
+
15
+ ## Basic
16
+
17
+ ```tsx
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuLabel,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuShortcut,
25
+ DropdownMenuTrigger,
26
+ } from '@oxyhq/bloom/dropdown-menu';
27
+ import { Button } from '@oxyhq/bloom/button';
28
+
29
+ <DropdownMenu>
30
+ <DropdownMenuTrigger asChild label="Open menu">
31
+ <Button variant="secondary">Open menu</Button>
32
+ </DropdownMenuTrigger>
33
+
34
+ <DropdownMenuContent>
35
+ <DropdownMenuLabel>My account</DropdownMenuLabel>
36
+ <DropdownMenuSeparator />
37
+ <DropdownMenuItem onPress={openProfile}>Profile</DropdownMenuItem>
38
+ <DropdownMenuItem trailing={<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>}>
39
+ Settings
40
+ </DropdownMenuItem>
41
+ <DropdownMenuItem disabled>Billing</DropdownMenuItem>
42
+ <DropdownMenuSeparator />
43
+ <DropdownMenuItem variant="destructive" onPress={signOut}>
44
+ Sign out
45
+ </DropdownMenuItem>
46
+ </DropdownMenuContent>
47
+ </DropdownMenu>
48
+ ```
49
+
50
+ A row dismisses the menu after it fires. Pass `keepOpen` to a row that should not — a checkbox row you expect the user to toggle twice, say.
51
+
52
+ ## `asChild`
53
+
54
+ `asChild` is Radix's composition escape hatch and it works the same way here: the trigger's single element child *becomes* the trigger and receives the open handler plus the accessibility props. Without it, the children render inside Bloom's own pressable.
55
+
56
+ ```tsx
57
+ <DropdownMenuTrigger asChild label="Actions">
58
+ <Button variant="icon" onPress={track}>…</Button>
59
+ </DropdownMenuTrigger>
60
+ ```
61
+
62
+ Handlers the child already carries are composed, not replaced — `track()` runs first, then the menu opens.
63
+
64
+ `disabled` on the trigger is guarded the same way, at the composition layer rather than on the child alone — pass `disabled` to `DropdownMenuTrigger`, not to your own child element, or the open handler still fires.
65
+
66
+ ## Controlled
67
+
68
+ ```tsx
69
+ const [open, setOpen] = useState(false);
70
+
71
+ <DropdownMenu open={open} onOpenChange={setOpen}>
72
+
73
+ </DropdownMenu>
74
+ ```
75
+
76
+ Uncontrolled by default; pass `open` to take over. `onOpenChange` fires either way. There is no imperative control to reach for and no `close()` to call: the menu's state is one boolean, and a row closes it by returning.
77
+
78
+ ## Checkbox and radio rows
79
+
80
+ ```tsx
81
+ <DropdownMenuCheckboxItem checked={dense} onCheckedChange={setDense} keepOpen>
82
+ Dense rows
83
+ </DropdownMenuCheckboxItem>
84
+
85
+ <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
86
+ <DropdownMenuRadioItem value="newest">Newest</DropdownMenuRadioItem>
87
+ <DropdownMenuRadioItem value="oldest">Oldest</DropdownMenuRadioItem>
88
+ </DropdownMenuRadioGroup>
89
+ ```
90
+
91
+ Both announce their state as `aria-checked`, which is the spelling react-native-web reads and React Native folds back into `accessibilityState` — so it reaches assistive tech on both platforms.
92
+
93
+ ## Sub-menus: a flyout on web, an inline disclosure on native
94
+
95
+ The API is one thing on both platforms — a shadcn call site ports across unchanged — and the presentation is the platform's own, matching what each platform's upstream does.
96
+
97
+ ```tsx
98
+ <DropdownMenuSub>
99
+ <DropdownMenuSubTrigger>Send to…</DropdownMenuSubTrigger>
100
+ <DropdownMenuSubContent>
101
+ <DropdownMenuItem>Email</DropdownMenuItem>
102
+ <DropdownMenuItem>Messages</DropdownMenuItem>
103
+ </DropdownMenuSubContent>
104
+ </DropdownMenuSub>
105
+ ```
106
+
107
+ **On web** the sub-panel flies out beside its trigger row, the way shadcn's does, and its chevron points right:
108
+
109
+ - Hovering the row opens it; leaving either surface schedules a close 300 ms later, and entering either one cancels that — which is what keeps it open while the pointer travels diagonally across the gap between them.
110
+ - Pressing the row toggles it, so a click and a touch-web tap both work.
111
+ - <kbd>→</kbd> opens it and moves focus onto its first row; <kbd>←</kbd> closes it and returns focus to the trigger.
112
+ - <kbd>Esc</kbd> closes the **innermost** surface first: one press shuts the sub-panel and leaves the menu open, a second shuts the menu.
113
+ - It takes its own rank from `overlay/stack.ts` on mount, like every other surface, so it paints above the menu it opened from and carries no `zIndex` of its own.
114
+ - It positions itself through the same `overlay/dropdown-placement` every other anchored surface uses, with `side="right"`. With no room to the right it flips to the left of the row on its own — there is no second positioner.
115
+
116
+ **On native** it is an inline disclosure: the trigger expands its content in place, under the row, and the chevron swaps between down and up. react-native-reusables renders it the same way there, and a menu on native is a bottom sheet — there is nowhere for a flyout to fly to.
117
+
118
+ The split is a build-time one, not a `Platform.OS` branch: `floating/menu-sub-flyout.tsx` and `floating/menu-sub-inline.tsx` are passed to `createMenuRows` by each family's own platform fork, so the web-only `FloatingPanel` never reaches a native bundle.
119
+
120
+ Verified in a real browser by `scripts/verify-submenu-flyout.mjs` — jest can see none of it.
121
+
122
+ ## Placement (web)
123
+
124
+ `DropdownMenuContent` takes `side` (`'top' | 'bottom'`, default `'bottom'`), `align` (`'start' | 'center' | 'end'`, default `'start'`), `sideOffset` and `alignOffset` — the same four Radix takes. They are preferences: the surface flips to the other side when the named one does not fit and clamps into the viewport when neither does.
125
+
126
+ `side` also accepts `'left'` and `'right'`, which make the horizontal axis the one that fits, flips and clamps, and `align` the vertical one. That is how the sub-panel positions itself; a root dropdown has no use for it.
127
+
128
+ On native these are ignored. The menu is a bottom sheet, which is anchored to the screen rather than to the trigger.
129
+
130
+ ## Naming a row that is not a plain string
131
+
132
+ A row whose child is a string names itself. A row composed of elements does not:
133
+
134
+ ```tsx
135
+ <DropdownMenuItem accessibilityLabel="Profile">
136
+ <UserIcon />
137
+ <Text>Profile</Text>
138
+ </DropdownMenuItem>
139
+ ```
140
+
141
+ Without it a screen reader announces the row as nothing. This is where the upstream primitive's `textValue` lands — spelled `accessibilityLabel` because in Bloom that is all it does.
142
+
143
+ ## Screen-reader focus
144
+
145
+ On NATIVE, opening a menu moves the VoiceOver/TalkBack cursor into the sheet, and closing it returns the cursor to the trigger. Both are automatic for every Bloom surface that presents through a sheet — menus, context menus, the menubar, popovers and select — and neither needs any wiring at the call site.
146
+
147
+ They are a pair on purpose. Moving the cursor in without giving it back strands it on the root view after the surface unmounts, which is worse than never moving it.
148
+
149
+ The platform's own dismiss gesture works too: a two-finger Z scrub under VoiceOver, a back gesture under TalkBack. Android's hardware back already closed these, since every Bloom sheet is an RN `<Modal>`.
150
+
151
+ On WEB none of this applies — the browser owns focus and the hooks return early rather than calling an API that is a no-op there.
152
+
153
+ ## Rows are className-first
154
+
155
+ Every row is one pressable node built from Bloom's `styled()` primitives, shared by all three menu families through `floating/shared.tsx` — so the disabled treatment, the destructive colour, the pointer highlight and the ARIA state stay in one place and a fix lands in every menu at once.
156
+
157
+ Its shape is a class string, not resolved numbers, because that is how the originals are built and it is the only form a consumer can override with a utility (an inline `style` outranks a class on web whatever the array order). `floating/constants.ts` holds the vocabulary: `relative flex-row items-center px-2 py-1.5 gap-2 min-h-8 rounded-xl` on a row, `pl-8 pr-2` on the two that carry an indicator, `text-sm` throughout, and a 32px minimum height. There is no `sm:` breakpoint any more — the row is the same density at every window width.
158
+
159
+ Every part takes a `className` and APPENDS it to its own, so a layout class cannot strip the chrome. Two utilities for one property are then resolved by Tailwind's own emission order, exactly as in any shadcn consumer; `style` remains the unambiguous override.
160
+
161
+ **A web consumer must have the Tailwind/NativeWind pipeline wired and `@source`-scanning `@oxyhq/bloom/lib`.** That was always required; it is now visible rather than subtle, because the colours moved out of inline style too.
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: ErrorBoundary
3
+ description: Containing a failure to a section instead of losing the screen.
4
+ order: 51
5
+ ---
6
+
7
+ # ErrorBoundary
8
+
9
+ Catches a render error below it and shows a fallback instead of unmounting the
10
+ tree above.
11
+
12
+ ```tsx
13
+ import { ErrorBoundary } from '@oxyhq/bloom/error-boundary';
14
+
15
+ <ErrorBoundary
16
+ title="This feed didn't load"
17
+ message="Your other tabs are unaffected."
18
+ retryLabel="Try again"
19
+ onError={report}
20
+ >
21
+ <Feed />
22
+ </ErrorBoundary>
23
+ ```
24
+
25
+ ## Which prop decides what
26
+
27
+ | prop | decides |
28
+ | ------------ | ------------------------------------------------------------ |
29
+ | `title` / `message` / `retryLabel` | the wording of the default fallback |
30
+ | `fallback` | replaces it entirely — a node, or a function |
31
+ | `onError` | reporting; called with the error and the React error info |
32
+
33
+ ## Place it where a failure should cost a section, not the screen
34
+
35
+ Around a feed, a widget, a tab's content, a third-party embed. One boundary at
36
+ the app root turns every error into a blank page, which is the outcome a
37
+ boundary exists to prevent.
38
+
39
+ The useful question is: *if this subtree fails, what should the user still be
40
+ able to do?* Whatever the answer is, the boundary goes below it.
41
+
42
+ ## The function `fallback` gives you `retryCount`
43
+
44
+ That is the reason to reach for it. After two or three failed retries, offering
45
+ the button again is dishonest — the same thing will fail the same way.
46
+
47
+ ```tsx
48
+ fallback={({ error, retry, retryCount }) =>
49
+ retryCount < 2 ? <Retry onPress={retry} /> : <Offline error={error} />
50
+ }
51
+ ```
52
+
53
+ ## Write the fallback for someone who does not know what a boundary is
54
+
55
+ Say what they lost and what the button will do. "This feed didn't load" is
56
+ useful; "Something went wrong" tells them only that we noticed.
57
+
58
+ Never show the error message itself in production UI. It is written for us, it
59
+ often contains internals, and it cannot be acted on.
60
+
61
+ ## What it does not catch
62
+
63
+ React error boundaries catch errors thrown **during render**. They do not catch:
64
+
65
+ - errors in event handlers — those are just exceptions; catch them where they
66
+ happen and surface them with a [toast](/docs/bloom/main/toast) or an
67
+ [admonition](/docs/bloom/main/admonition);
68
+ - rejected promises and async failures — the same;
69
+ - errors in code above the boundary itself.
70
+
71
+ So a boundary is a safety net for the unexpected, not the way you handle a
72
+ failed request. A request that can fail has a designed failure state.
package/docs/fab.mdx ADDED
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Fab
3
+ description: The floating action button — one per screen, for the screen's primary action.
4
+ order: 43
5
+ ---
6
+
7
+ # Fab
8
+
9
+ A floating button for the **one** thing a screen is mostly for: compose on a
10
+ feed, add on a list, new message in an inbox.
11
+
12
+ ```tsx
13
+ import { Fab } from '@oxyhq/bloom/fab';
14
+
15
+ <Fab
16
+ icon={<PencilIcon />}
17
+ accessibilityLabel="New post"
18
+ onPress={compose}
19
+ />
20
+
21
+ <Fab
22
+ icon={<PencilIcon />}
23
+ label="New post"
24
+ placement="bottom-right"
25
+ onPress={compose}
26
+ />
27
+ ```
28
+
29
+ ## Which prop decides what
30
+
31
+ | prop | decides |
32
+ | ----------- | --------------------------------------------------------------- |
33
+ | `icon` | the glyph |
34
+ | `label` | makes it an extended FAB with visible text |
35
+ | `variant` | `primary` · `secondary` · `tertiary` · `surface` |
36
+ | `size` | `small` · `medium` · `large`, or a number |
37
+ | `placement` | which corner it floats in, or `static` to place it yourself |
38
+ | `offset` | distance from the edges |
39
+
40
+ ## An icon-only FAB needs `accessibilityLabel`
41
+
42
+ Without it the button announces as "button" and nothing else. A pencil is not
43
+ self-describing to a screen reader, and it is not always self-describing on
44
+ screen either — which is what `label` is for.
45
+
46
+ ## One per screen
47
+
48
+ A FAB claims the corner and floats above the content. Two of them are two
49
+ primary actions, which means neither is primary, and they compete for the same
50
+ thumb position. If a screen has several important actions, the FAB takes the
51
+ most common one and the rest go in the content or a menu.
52
+
53
+ ## `placement="static"` when something else owns the position
54
+
55
+ A FAB inside a scroll container, or one that has to sit above a tab bar, is
56
+ easier to place with layout than to nudge with `offset`. `static` turns off the
57
+ absolute positioning and leaves it to you.
58
+
59
+ Remember the safe area and any bottom chrome: a FAB pinned to `bottom-right`
60
+ with no offset lands under a tab bar on most phones, where it is unreachable
61
+ rather than merely ugly.
62
+
63
+ ## Do not hide it behind scroll
64
+
65
+ Hiding the FAB while scrolling down is a common pattern and a bad default: the
66
+ action becomes unavailable exactly while the user is looking for it, and the
67
+ reappear animation reads as a glitch. Prefer letting it stay.
package/docs/field.mdx ADDED
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: Field
3
+ description: Label, description, error and the wiring that makes them announce as one control.
4
+ order: 29
5
+ ---
6
+
7
+ # Field
8
+
9
+ `Field` wraps a control with the things around it — a label, a hint, an error —
10
+ and, more importantly, the associations that make a screen reader read them as
11
+ belonging to that control rather than as loose text near it.
12
+
13
+ ```tsx
14
+ import { Field } from '@oxyhq/bloom/field';
15
+ import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
16
+
17
+ <Field
18
+ label="Email address"
19
+ description="We only use this for sign-in."
20
+ error={errors.email}
21
+ required
22
+ >
23
+ <TextField>
24
+ <TextFieldInput label="Email" value={email} onChangeText={setEmail} />
25
+ </TextField>
26
+ </Field>
27
+ ```
28
+
29
+ ## Which prop decides what
30
+
31
+ | prop | decides |
32
+ | ------------- | ------------------------------------------------------------ |
33
+ | `label` | the control's name |
34
+ | `description` | a persistent hint, shown whether or not there is an error |
35
+ | `error` | the message; `null` or omitted means valid |
36
+ | `required` | the required marker and the announced requirement |
37
+ | `disabled` | dims the whole group |
38
+ | `nativeID` | the id the label and control are wired through |
39
+
40
+ ## `error` is a string, not a boolean
41
+
42
+ Because the message *is* the value. A boolean `hasError` forces the message to
43
+ live somewhere else, and the two then disagree — a red border with no
44
+ explanation, or an explanation after the user already fixed it.
45
+
46
+ ```tsx
47
+ <Field error={touched ? validate(value) : null}>
48
+ ```
49
+
50
+ ## A description is not an error, and an error is not a description
51
+
52
+ Keep both. The description says what the field is for and stays put; the error
53
+ says what went wrong with what was typed. Replacing the description with the
54
+ error means the user loses the instructions at the exact moment they need them.
55
+
56
+ ## Placeholder is not a label
57
+
58
+ A placeholder disappears as soon as there is text, so a form filled in from
59
+ autofill or a paste has no visible labels at all. `Field`'s `label` is the
60
+ label; a placeholder is at most an example of the format.
61
+
62
+ ## Composition
63
+
64
+ `Field` does not care what it wraps — a `TextField`, a `Switch`, a
65
+ `SegmentedControl`, a custom control. It supplies the label/description/error
66
+ and the id wiring; the control supplies its own role and state.
67
+
68
+ For the label alone, without the surrounding structure, use
69
+ [`Label`](/docs/bloom/main/label). For a control with attached buttons or
70
+ prefixes, use [`InputGroup`](/docs/bloom/main/input-group) *inside* a `Field`.
package/docs/fill.mdx ADDED
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Fill
3
+ description: The four-line absolute-inset block, as one word.
4
+ order: 46
5
+ ---
6
+
7
+ # Fill
8
+
9
+ A view pinned to all four edges of its parent. It replaces the style object
10
+ that used to be written inline wherever a scrim, a gradient or a press layer
11
+ had to cover its parent exactly.
12
+
13
+ ```tsx
14
+ import { Fill } from '@oxyhq/bloom/fill';
15
+
16
+ <View style={{ height: 180, borderRadius: 12, overflow: 'hidden' }}>
17
+ <Image source={cover} style={{ flex: 1 }} />
18
+ <Fill style={{ backgroundColor: 'rgba(0,0,0,0.45)' }} />
19
+ <Fill style={{ alignItems: 'center', justifyContent: 'center' }}>
20
+ <Text>Over the top</Text>
21
+ </Fill>
22
+ </View>
23
+ ```
24
+
25
+ ## What it is for
26
+
27
+ Writing `position: 'absolute', top: 0, right: 0, bottom: 0, left: 0` by hand
28
+ four times in a file is how three of the four edges end up right. `Fill` is the
29
+ same object, named.
30
+
31
+ Overriding one edge still works, and the others survive:
32
+
33
+ ```tsx
34
+ <Fill style={{ left: '50%' }} /> // right half only
35
+ ```
36
+
37
+ ## It takes its size from the parent
38
+
39
+ `Fill` has no size of its own — it fills the nearest positioned ancestor. In a
40
+ parent with no height it is invisible, not broken, which is the usual reason a
41
+ scrim "doesn't show up".
42
+
43
+ ## Stacking
44
+
45
+ Two fills stack in source order: the later one paints above. There is no
46
+ `zIndex` involved, and there should not be — a scrim over a photo under a label
47
+ is three siblings in the order you want them.
48
+
49
+ ## What it is not for
50
+
51
+ - **An overlay surface.** A dialog, a sheet or a menu goes through
52
+ [`Portal`](/docs/bloom/main/portal) plus
53
+ [`OverlayRoot`](/docs/bloom/main/overlay), which give it a stack position and
54
+ an interactive root. A `Fill` at the root of a screen is a full-bleed band
55
+ that silently eats presses.
56
+ - **A press layer that must not block touches.** If content underneath has to
57
+ stay pressable, the fill needs `pointerEvents="none"` — as a **prop**, since
58
+ the RN-only values are dropped when written as a style.
package/docs/fonts.mdx ADDED
@@ -0,0 +1,144 @@
1
+ ---
2
+ title: Fonts
3
+ description: How Bloom's typefaces reach a web bundle and a native build — and where the documented stacks and current fleet practice diverge.
4
+ order: 58
5
+ ---
6
+
7
+ # Fonts
8
+
9
+ Bloom ships its own typefaces and loads them for you. `BloomThemeProvider`
10
+ mounts `FontLoader`, which injects `@font-face` rules on web and loads the
11
+ assets on native.
12
+
13
+ ```tsx
14
+ // Nothing to do: BloomThemeProvider already does this.
15
+ <BloomThemeProvider>…</BloomThemeProvider>
16
+ ```
17
+
18
+ ## What the subpath exports
19
+
20
+ Almost no app imports `@oxyhq/bloom/fonts` — the provider does. Reach for it
21
+ when you need the family stacks in a style of your own, or when you are loading
22
+ fonts yourself (a bare React Native app with no `BloomThemeProvider`):
23
+
24
+ ```tsx
25
+ import { fontFamilies, fontCssVars, FONT_ASSETS, applyFontFaces, FontLoader } from '@oxyhq/bloom/fonts';
26
+ ```
27
+
28
+ | Export | What it is |
29
+ | --- | --- |
30
+ | `fontFamilies` | The three family STACKS (`display`, `sans`, `mono`), each with system fallbacks. The literal `fontFamily` string source on native, and the value of the CSS variables on web. |
31
+ | `fontCssVars` | The inverse map: family name → `--bloom-font-*` custom property name. |
32
+ | `FONT_ASSETS` | The `expo-font` map for `useFonts()`. Populated under Metro only — see below. |
33
+ | `applyFontFaces()` | Injects the `@font-face` rules and the `:root` variables. Web only, idempotent, SSR-safe. |
34
+ | `FontLoader` | The component `BloomThemeProvider` mounts. `enabled`, `fallback`, `children`. |
35
+
36
+ `fontFamilies.display` and `.sans` both lead with **BlomusModernus**;
37
+ `.mono` leads with **Geist Mono**. On native, `FontLoader` prepends
38
+ `{ fontFamily: 'BlomusModernus' }` to `Text.defaultProps.style` once the assets
39
+ report loaded, so every `<Text>` in the app inherits it — which is why an app
40
+ setting its own `fontFamily` is overriding Bloom rather than complementing it.
41
+
42
+ ## A fourth family ships and registers unconditionally: Inter
43
+
44
+ Bloom bundles Inter alongside BlomusModernus and Geist Mono, on both platforms,
45
+ and loading it is not gated the way you'd expect:
46
+
47
+ - `FONT_ASSETS` (native) requires `InterVariable.ttf` (879,708 bytes) under the
48
+ key `Inter`. `FontLoader.native.tsx` calls `useFonts(FONT_ASSETS)`
49
+ unconditionally — per the Rules of Hooks the hook has to fire whether or not
50
+ `<BloomThemeProvider fonts={false}>` is set — so Inter is bundled into the
51
+ native binary and loaded into the platform font registry for every consumer
52
+ that mounts `BloomThemeProvider`, regardless of that prop.
53
+ - `apply-font-faces.web.ts` injects an `@font-face` rule for `'Inter'` from
54
+ `InterVariable.woff2` (48,256 bytes) alongside the other two families. This
55
+ half *is* gated on the `fonts` prop (default `true`).
56
+
57
+ **But `Inter` is not a member of `fontFamilies.display`, `.sans` or `.mono`** —
58
+ those three stacks lead with BlomusModernus and Geist Mono only, as shown
59
+ above. There is no exported constant that names Inter; a consumer reaches it
60
+ only by writing the literal string `'Inter'` itself.
61
+
62
+ In practice, several apps in the fleet do exactly that. Mention's
63
+ `packages/frontend/global.css` and `tailwind.config.js`, and CrowdSource's
64
+ `console` and `reviewer` apps, set `--font-sans: Inter` / `sans: ['Inter', …]`
65
+ directly. Alia's `packages/app/tailwind.config.js` does the same — and ships
66
+ no font file of its own for it: the built `InterVariable-*.woff2` in
67
+ `alia-canvas`, `alia-console` and `alia-gateway-admin` is byte-identical
68
+ (md5 `260c81a4759baf163c025001c4f27872`, 48,256 bytes) to Bloom's own
69
+ `src/fonts/assets/InterVariable.woff2`. This resolves today only because
70
+ Bloom happens to register the family, unconditionally, on both platforms.
71
+
72
+ `'Inter-Regular'`, which appears as a fallback in Mention's and CrowdSource's
73
+ `font-family` declarations (`font-family: 'Inter', 'Inter-Regular', sans-serif`),
74
+ names nothing Bloom — or anything else in this repo — ever registers. Only the
75
+ bare family `Inter` is registered, as a single variable face. That fallback
76
+ entry has been dead since it was written; on a machine without a
77
+ system-installed `Inter-Regular` it just falls through to the next name in the
78
+ stack.
79
+
80
+ Whether selecting `'Inter'` by hand is a supported way to reach it, or several
81
+ apps happening to line up with a family Bloom bundles for its own reasons, is
82
+ not settled anywhere in this repo. Treat it as an open question, not a green
83
+ light or a ban — and treat any *other* hand-set literal the same way `'Inter-Regular'`
84
+ turned out: it either matches a family one of the four platform conditions
85
+ above actually registers, or it silently falls back to the system face.
86
+
87
+ ## Four ways in, one surface
88
+
89
+ `./fonts` is resolved by export condition, and each condition lands on a barrel
90
+ whose members are already the right implementation for that platform. This is
91
+ the whole mechanism — there is no runtime platform check anywhere in the family:
92
+
93
+ | Condition | Barrel | `applyFontFaces` | `FONT_ASSETS` | `FontLoader` |
94
+ | --- | --- | --- | --- | --- |
95
+ | `react-native` (Metro) | `index.ts` | no-op stub | the Metro asset map | loads `.ttf` via `useFonts` |
96
+ | `browser` (Vite, webpack, Metro-web) | `index.web.ts` | injects `@font-face` | empty | injects on first render |
97
+ | `node` (SSR, prerender, tooling) | `index.node.ts` | no-op stub | empty | renders children |
98
+ | `import` / `require` | `index.ts` | no-op stub | empty | — |
99
+
100
+ The `node` condition is load-bearing, not tidiness: the web `FontLoader` imports
101
+ `apply-font-faces.web`, which reaches `font-urls.web`'s `.woff2` imports. Node
102
+ hands a `.woff2` to the JS parser, so merely LOADING that module throws a
103
+ `SyntaxError` — before any `typeof document` guard inside the function can run.
104
+
105
+ Every barrel names its platform-forked siblings OUTRIGHT (`./apply-font-faces.web`,
106
+ `./font-assets.web`) rather than leaving the extension to the bundler. Vite does
107
+ no platform-extension resolution, so an extensionless specifier there falls
108
+ through to the default file and the fonts silently stop being injected.
109
+
110
+ ## The `.woff2` files are assets, not base64
111
+
112
+ `font-urls.web.ts` imports the four files so the consuming bundler emits them
113
+ as content-hashed assets. **Never inline them as base64 again** — that put
114
+ ~219 KB gzip into every web entry bundle and made the fonts uncacheable, since
115
+ a font embedded in JS cannot be cached as a font.
116
+
117
+ The one consumer requirement is `woff2` in Metro's `assetExts`, which
118
+ `@oxyhq/app-preset` registers fleet-wide. Bundlers that already treat an asset
119
+ import as a URL (Vite, Rollup, webpack 5) need nothing.
120
+
121
+ ## Two things that look like cleanups and are not
122
+
123
+ **`apply-font-faces.ts` must stay an empty stub with no imports.** It is the
124
+ neutral default; the web variant is `apply-font-faces.web.ts`. Give the default
125
+ real imports and Metro bundles a second, unusable copy of the font machinery
126
+ into every native app.
127
+
128
+ **Do not fork `FontLoader` into a `.web` plus a neutral default.**
129
+ `BloomThemeProvider` imports it by *relative* path, and export conditions do
130
+ not apply to relative specifiers — so Vite would silently take the neutral
131
+ default and stop injecting `@font-face` for every web consumer. `FontLoader.tsx`
132
+ IS the web implementation; `FontLoader.native.tsx` and `FontLoader.node.tsx` are
133
+ selected by Metro's platform extensions and by the `node` condition respectively.
134
+
135
+ ## Verifying that fonts are actually on
136
+
137
+ This is a system that fails by looking fine: a missing `@font-face` falls back
138
+ to a system face rather than erroring. Bloom's own Storybook ran with
139
+ `fonts={false}` for months and every story looked plausible.
140
+
141
+ Check the computed `font-family` on real text, not that the provider is
142
+ mounted. On web, `applyFontFaces()` goes through a CONSTRUCTED stylesheet
143
+ (`adoptedStyleSheets`), so there is no `<style id="bloom-fonts">` element to
144
+ look for — read the document's adopted sheets, or just measure the text.