@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
package/docs/item.mdx ADDED
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Item
3
+ description: The one row primitive — and the `role` prop that decides how it is announced.
4
+ order: 31
5
+ ---
6
+
7
+ # Item
8
+
9
+ A row: optional leading element, a title, an optional subtitle, an optional
10
+ trailing element. It is what menus, option lists, comboboxes and settings rows
11
+ are built from, so a row anywhere in the ecosystem has the same height, the same
12
+ press feedback and the same disabled treatment.
13
+
14
+ ```tsx
15
+ import { Item } from '@oxyhq/bloom/item';
16
+
17
+ <Item
18
+ leading={<Avatar source={user.avatar} size={32} />}
19
+ title={user.displayName}
20
+ subtitle={`@${user.username}`}
21
+ trailing={selected ? <CheckIcon /> : null}
22
+ onPress={() => choose(user)}
23
+ role="option"
24
+ />
25
+ ```
26
+
27
+ ## Which prop decides what
28
+
29
+ | prop | decides |
30
+ | ------------- | --------------------------------------------------------------- |
31
+ | `title` / `subtitle` | the two text lines |
32
+ | `leading` / `trailing` | the slots on either side |
33
+ | `children` | replaces the title/subtitle block entirely |
34
+ | `onPress` | makes the row pressable |
35
+ | `role` | **how it is announced** — see below |
36
+ | `selected` | the selected state, for `option` / `radio` rows |
37
+ | `active` | the "you are here" state, for navigation rows |
38
+ | `density` | `comfortable` (default) or `compact` for a dense list |
39
+ | `destructive` | paints the title in the error role |
40
+
41
+ ## `role` is the prop that matters
42
+
43
+ A row looks the same in every context and means something different in each.
44
+ `role` is how you say which:
45
+
46
+ | `role` | for |
47
+ | ---------- | ----------------------------------------------- |
48
+ | `option` | a choice in a listbox — a combobox, a picker |
49
+ | `radio` | a mutually exclusive choice |
50
+ | `menuitem` | an action in a menu |
51
+ | `listitem` | a row in a plain list |
52
+
53
+ Getting this wrong is invisible on screen and wrong in every screen reader: a
54
+ combobox whose rows are `listitem` announces as a list, and the user is never
55
+ told they are choosing.
56
+
57
+ ## `selected` and `active` are different states
58
+
59
+ `selected` means *this is the chosen one* — a checkmark in a picker. `active`
60
+ means *this is where you are* — the current route in a sidebar. They look
61
+ similar and they are not the same claim: a sidebar row is active without the
62
+ user having selected anything, and a picker row is selected whether or not it
63
+ is focused.
64
+
65
+ ## `destructive` colours, it does not confirm
66
+
67
+ It paints the title in the error role. It does not add a confirmation step —
68
+ if the action is irreversible, `confirm()` from
69
+ [`surfaces`](/docs/bloom/main/surfaces) is what asks.
package/docs/kbd.mdx ADDED
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Kbd
3
+ description: A keyboard key rendered as a key — in menus, palettes and help text.
4
+ order: 44
5
+ ---
6
+
7
+ # Kbd
8
+
9
+ Renders a keyboard key with the raised-cap look, so a shortcut in running text
10
+ reads as a key rather than as a stray capital letter.
11
+
12
+ ```tsx
13
+ import { Kbd } from '@oxyhq/bloom/kbd';
14
+
15
+ <Text>
16
+ Press <Kbd>⌘</Kbd> <Kbd>K</Kbd> to open the command palette.
17
+ </Text>
18
+
19
+ <Kbd size="sm">Esc</Kbd>
20
+ ```
21
+
22
+ ## Which prop decides what
23
+
24
+ | prop | decides |
25
+ | ------ | ------------------------------------------------- |
26
+ | `size` | `sm` for inside a row or a menu, `md` for prose |
27
+
28
+ ## One key per `Kbd`
29
+
30
+ `⌘K` as a single child renders one wide cap, which is not what a keyboard looks
31
+ like and not what the user has to press. Two keys, two `Kbd`s.
32
+
33
+ ## Use the platform's own symbols
34
+
35
+ `⌘` on Apple platforms, `Ctrl` elsewhere; `⌥` versus `Alt`; `⇧` versus `Shift`.
36
+ A shortcut written for the wrong platform is worse than none — the user presses
37
+ a key that does nothing and concludes the feature is broken.
38
+
39
+ `Kbd` renders what you give it; deciding which spelling to give it belongs to
40
+ the app, which knows the platform.
41
+
42
+ ## It is decoration, not a control
43
+
44
+ `Kbd` draws a key. It does not register a shortcut, and pressing it does
45
+ nothing. Binding keys belongs to the app or the surface that owns the key
46
+ handling — the same reason [`Command`](/docs/bloom/main/command)'s `shortcut`
47
+ field is a hint rather than a binding.
48
+
49
+ ## Where it earns its place
50
+
51
+ - A [`Command`](/docs/bloom/main/command) row, showing the shortcut for the
52
+ action.
53
+ - A menu item, for the same reason.
54
+ - Help and onboarding text, where the key is the instruction.
55
+
56
+ Not in a tooltip on a touch device: a user with no keyboard reading about `⌘K`
57
+ is being told about a feature they cannot reach.
package/docs/label.mdx ADDED
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: Label
3
+ description: A control's name, wired to the control rather than merely sitting above it.
4
+ order: 33
5
+ ---
6
+
7
+ # Label
8
+
9
+ The text that names a control. Use it when you are laying a form out yourself;
10
+ [`Field`](/docs/bloom/main/field) renders one for you and handles the wiring.
11
+
12
+ ```tsx
13
+ import { Label } from '@oxyhq/bloom/label';
14
+
15
+ <Label nativeID="workspace-name" required>Workspace name</Label>
16
+ <TextField>
17
+ <TextFieldInput nativeID="workspace-name" label="Workspace name" value={name} onChangeText={setName} />
18
+ </TextField>
19
+ ```
20
+
21
+ ## Which prop decides what
22
+
23
+ | prop | decides |
24
+ | ---------- | ------------------------------------------------------------- |
25
+ | `nativeID` | the id the control points at — this is what makes it a label |
26
+ | `htmlFor` | the reverse association on web |
27
+ | `required` | the required marker, and that it is announced as required |
28
+ | `disabled` | dims it in step with the control |
29
+ | `size` | `xs` · `sm` · `md` |
30
+
31
+ ## Without the wiring it is just text above a box
32
+
33
+ A `<Text>` above an input is not a label: a screen reader reaches the input and
34
+ announces "edit text, blank". The association is the whole feature — either
35
+ through `nativeID`/`htmlFor` here, or by letting `Field` do it.
36
+
37
+ On web it also buys a click target: clicking the label focuses the control.
38
+
39
+ ## `required` is a claim, not a decoration
40
+
41
+ The marker and the announced requirement come from the same prop, so they
42
+ cannot disagree. Do not draw your own asterisk beside the text — it is
43
+ invisible to a screen reader and produces a field that looks required and
44
+ announces as optional.
45
+
46
+ ## Do not use a label as a heading
47
+
48
+ A label names a control. A section title above a group of fields is a heading;
49
+ using `Label` for it makes a screen reader look for a control that is not
50
+ there.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: LinkPreviewCard
3
+ description: An unfurled link — structural props, so Bloom never depends on the contracts package.
4
+ order: 59
5
+ ---
6
+
7
+ # LinkPreviewCard
8
+
9
+ The card that renders an unfurled link: cover image, site name, title,
10
+ description.
11
+
12
+ ```tsx
13
+ import { LinkPreviewCard } from '@oxyhq/bloom/link-preview';
14
+
15
+ <LinkPreviewCard
16
+ url={preview.url}
17
+ siteName={preview.siteName}
18
+ title={preview.title}
19
+ description={preview.description}
20
+ image={preview.image}
21
+ />
22
+ ```
23
+
24
+ ## Which prop decides what
25
+
26
+ | prop | decides |
27
+ | ------------- | ----------------------------------------------------------- |
28
+ | `url` | where it goes, and the fallback for everything else |
29
+ | `title` / `description` / `siteName` / `image` | the unfurled metadata |
30
+ | `coverFill` | whether the cover crops to fill or fits |
31
+ | `onPress` | overrides the default "open the URL" |
32
+
33
+ ## Only `url` is required
34
+
35
+ Everything else is optional because unfurling fails all the time — a site with
36
+ no Open Graph tags, a link behind auth, a timeout. The hostname is the fallback
37
+ for both the site name and the title, so a bare URL still renders a usable card
38
+ rather than an empty box with a shadow.
39
+
40
+ Design the calling code for that: pass whatever the unfurler returned, without
41
+ deciding not to render when the metadata is thin.
42
+
43
+ ## The shape is structural, not a contracts type
44
+
45
+ The props mirror the platform's `LinkPreview` DTO but are declared
46
+ independently, so Bloom does not depend on `@oxyhq/contracts`. If the DTO gains
47
+ a field, map it at the call site.
48
+
49
+ ## `image` is a plain URL, not a file id
50
+
51
+ It is rendered directly with the RN `Image` primitive, **not** through
52
+ [`ImageResolver`](/docs/bloom/main/image-resolver) — an OG image comes from
53
+ whatever host the linked page uses, so there is no Oxy file id to resolve.
54
+
55
+ That is the opposite of the avatar rule, and the reason both are worth stating:
56
+ an avatar goes through the resolver, an OG image does not.
57
+
58
+ ## The chrome is `Card`'s
59
+
60
+ `outlined` at the `radius-20` rung. It used to be `bg-card` / `border-border`
61
+ classes, which are **inert on web until the consumer wires the Tailwind
62
+ pipeline** — the card then drew as an unbordered transparent block with no
63
+ error anywhere. As resolved inline tokens it paints either way.
64
+
65
+ The text inside is still className-first, and those class strings must stay
66
+ literal so a consumer's Tailwind content scan over `lib/` can see them.
package/docs/list.mdx ADDED
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: VirtualList
3
+ description: The one list primitive — windowed on web, virtualized on native, one prop shape.
4
+ order: 52
5
+ ---
6
+
7
+ # VirtualList
8
+
9
+ Bloom's list. On web it windows against the scroller; on native it is a
10
+ virtualized list. Same props either way.
11
+
12
+ ```tsx
13
+ import { VirtualList } from '@oxyhq/bloom/list';
14
+
15
+ <VirtualList
16
+ data={posts}
17
+ keyExtractor={(post) => post.id}
18
+ estimatedItemSize={96}
19
+ renderItem={({ item }) => <PostRow post={item} />}
20
+ ListEmptyComponent={<Empty query={query} />}
21
+ onEndReached={loadMore}
22
+ onEndReachedThreshold={0.5}
23
+ />
24
+ ```
25
+
26
+ ## Which prop decides what
27
+
28
+ | prop | decides |
29
+ | ----------------------- | ----------------------------------------------------- |
30
+ | `data` / `renderItem` | what is drawn |
31
+ | `keyExtractor` | identity across updates |
32
+ | `estimatedItemSize` | how the scrollbar behaves before rows are measured |
33
+ | `ListHeaderComponent` / `ListFooterComponent` / `ListEmptyComponent` | the slots |
34
+ | `onEndReached` / `onEndReachedThreshold` | pagination |
35
+ | `refreshing` / `onRefresh` | pull to refresh |
36
+
37
+ ## `estimatedItemSize` is why scrolling feels right or wrong
38
+
39
+ It is a hint, but a badly wrong one makes the scrollbar jump as real heights
40
+ replace the estimate. Estimate the **common** row, not the average of a bimodal
41
+ set: for a list that is mostly one-line rows with occasional images, the
42
+ one-line height is the better answer even though it is nowhere near the mean.
43
+
44
+ ## `ListEmptyComponent` is the one to wire first
45
+
46
+ An unstyled empty list is indistinguishable from a list that failed to load.
47
+ The empty state should say which — "No results for *bloom*" is a different
48
+ message from "Couldn't load your feed".
49
+
50
+ ## `onEndReached` can fire while a page is in flight
51
+
52
+ It fires once per approach to the end, not once per scroll event — but a second
53
+ approach can happen before the first request resolves. Guard on your own
54
+ loading flag; the list does not know what your fetch is doing.
55
+
56
+ ```tsx
57
+ onEndReached={() => { if (!isFetching && hasMore) fetchNext(); }}
58
+ ```
59
+
60
+ ## `keyExtractor` is not optional in practice
61
+
62
+ Without stable keys, rows recycle onto the wrong data: a list that reorders
63
+ shows the previous row's content for a frame, and inputs inside rows keep the
64
+ wrong value. Use the entity's id, never the index.
65
+
66
+ ## Native tuning props
67
+
68
+ `removeClippedSubviews`, `maxToRenderPerBatch`, `windowSize`,
69
+ `initialNumToRender`, `recycleItems` and `maintainVisibleContentPosition` are
70
+ honoured on native where applicable and accepted-but-ignored on web, where the
71
+ window virtualizer manages its own mounted-row budget. They are on the type so
72
+ one call site type-checks on both platforms.
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: Loading
3
+ description: Spinner, top bar, and inline loading indicators with one consistent API.
4
+ order: 13
5
+ ---
6
+
7
+ # Loading
8
+
9
+ One component, three variants. Pick the right one for the affordance:
10
+
11
+ - `spinner` — centered, optional text, the default
12
+ - `top` — sliding bar at the top of the screen for global progress
13
+ - `inline` — small spinner with a text label, for inline buttons or rows
14
+
15
+ For content placeholders reach for the `Skeleton` family
16
+ (`@oxyhq/bloom/skeleton`), which actually shimmers. `Loading` never had a real
17
+ skeleton — its `skeleton` variant drew static rectangles under the same name —
18
+ so the variant is gone.
19
+
20
+ ## Spinner (default)
21
+
22
+ ```tsx
23
+ import { Loading } from '@oxyhq/bloom';
24
+
25
+ <Loading variant="spinner" />
26
+ <Loading variant="spinner" size="large" />
27
+ <Loading variant="spinner" text="Loading…" />
28
+ <Loading variant="spinner" text="Saving…" iconSize={32} />
29
+ ```
30
+
31
+ ## Top bar
32
+
33
+ ```tsx
34
+ <Loading variant="top" showLoading={isFetching} />
35
+ ```
36
+
37
+ Renders a fixed-height bar at the top of the parent. Use it as a global progress indicator (route transitions, background sync). Requires `react-native-reanimated`.
38
+
39
+ ## Inline
40
+
41
+ ```tsx
42
+ <Loading variant="inline" text="Connecting…" />
43
+ ```
44
+
45
+ Spinner + label rendered in a row — small enough to drop into a list row or a button.
46
+
47
+ ## NativeWind className
48
+
49
+ For native-tailwind users, `className` overrides `color`:
50
+
51
+ ```tsx
52
+ <Loading variant="spinner" className="text-primary" />
53
+ ```
54
+
55
+ ## Props
56
+
57
+ ### Shared
58
+
59
+ | Prop | Type | Description |
60
+ |------|------|-------------|
61
+ | `variant?` | `'spinner' \| 'top' \| 'inline'` | Default: `'spinner'`. |
62
+ | `size?` | `'small' \| 'medium' \| 'large'` | Default: `'medium'`. |
63
+ | `color?` | `string` | Spinner color. Defaults to theme primary. |
64
+ | `className?` | `string` | NativeWind class. Overrides `color`. |
65
+ | `style?` | `ViewStyle` | Container style. |
66
+ | `showLoading?` | `boolean` | For animated variants — pause when false. |
67
+ | `testID?` | `string` | |
68
+
69
+ ### `spinner`
70
+
71
+ | Prop | Type | Description |
72
+ |------|------|-------------|
73
+ | `text?` | `string` | Optional label rendered under the spinner. |
74
+ | `textStyle?` | `TextStyle` | Label style. |
75
+ | `showText?` | `boolean` | Hide the label without removing layout. |
76
+ | `iconSize?` | `number` | Override the size prop's default icon px. |
77
+ | `spinnerIcon?` | `React.ReactNode` | Custom spinner (defaults to `ActivityIndicator`). |
78
+
79
+ ### `top`
80
+
81
+ | Prop | Type | Description |
82
+ |------|------|-------------|
83
+ | `iconSize?` | `number` | |
84
+ | `heightOffset?` | `number` | Offset from the top edge. |
85
+ | `spinnerIcon?` | `React.ReactNode` | |
86
+
87
+ ### `inline`
88
+
89
+ | Prop | Type | Description |
90
+ |------|------|-------------|
91
+ | `text?` | `string` | Label text. |
92
+ | `textStyle?` | `TextStyle` | |
93
+ | `spinnerIcon?` | `React.ReactNode` | |
94
+
95
+ ## Recipes
96
+
97
+ ### Lazy content placeholders
98
+
99
+ ```tsx
100
+ import * as Skeleton from '@oxyhq/bloom/skeleton';
101
+
102
+ const { data, isPending } = useQuery({ queryKey: ['articles'], queryFn });
103
+
104
+ if (isPending) {
105
+ return (
106
+ <Skeleton.Col style={{ gap: 8 }}>
107
+ {Array.from({ length: 5 }, (_, i) => <Skeleton.Text key={i} />)}
108
+ </Skeleton.Col>
109
+ );
110
+ }
111
+ return <ArticleList articles={data} />;
112
+ ```
113
+
114
+ ### Button busy state
115
+
116
+ Most of the time you want `<Button loading>` instead of an inline `Loading` — it preserves width. Reach for `inline` only when you can't use `Button`.
117
+
118
+ ### Global top progress
119
+
120
+ ```tsx
121
+ import { useIsFetching, useIsMutating } from '@tanstack/react-query';
122
+
123
+ function GlobalProgress() {
124
+ const fetches = useIsFetching();
125
+ const mutations = useIsMutating();
126
+ const busy = fetches > 0 || mutations > 0;
127
+ return <Loading variant="top" showLoading={busy} />;
128
+ }
129
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: MediaInsetBorder
3
+ description: A hairline drawn INSIDE a media edge, so a white photo still has a boundary.
4
+ order: 66
5
+ ---
6
+
7
+ # MediaInsetBorder
8
+
9
+ Draws a hairline just inside its own bounds, over whatever it wraps.
10
+
11
+ ```tsx
12
+ import { MediaInsetBorder } from '@oxyhq/bloom/media-inset-border';
13
+
14
+ <MediaInsetBorder style={{ borderRadius: 12 }}>
15
+ <Image source={photo} style={{ width: 320, height: 200 }} />
16
+ </MediaInsetBorder>
17
+ ```
18
+
19
+ ## Which prop decides what
20
+
21
+ | prop | decides |
22
+ | -------- | -------------------------------------------------------------- |
23
+ | `opaque` | whether the hairline is solid rather than a translucent overlay |
24
+
25
+ ## The problem it solves
26
+
27
+ A photo whose edges are white — a screenshot, a product shot on a light
28
+ background, a diagram — has no visible boundary on a light page. It bleeds into
29
+ the surface and the layout looks broken rather than seamless.
30
+
31
+ A border on the container does not fix it, because the image paints *over* the
32
+ container's inner edge at the corners. This draws the line on top, inside the
33
+ media's own bounds, so it survives the image's own corners and any clipping.
34
+
35
+ ## It goes around media, not around content
36
+
37
+ For a boundary around a text block or a group of controls, use
38
+ [`Card`](/docs/bloom/main/card)'s `outlined` variant or
39
+ [`Divider`](/docs/bloom/main/divider). This component exists specifically for
40
+ the case where the *content itself* can be the same colour as the page.
41
+
42
+ ## Match the radius
43
+
44
+ The inset border takes its shape from the style you give it, so a rounded photo
45
+ needs the same `borderRadius` here as on the image. A mismatch shows as a
46
+ hairline that clips the corners — subtle, and worse than no border.
47
+
48
+ ## `opaque`
49
+
50
+ Use the translucent default over photos, where a solid line reads as a frame.
51
+ Reach for `opaque` where the media is flat colour and the translucent line
52
+ disappears into it.
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Menubar
3
+ description: A horizontal bar of menus, at most one open at a time.
4
+ order: 20
5
+ ---
6
+
7
+ # Menubar
8
+
9
+ A desktop-style menu bar: a row of triggers, each opening a menu, with at most one open at a time.
10
+
11
+ New to Bloom in this release. Its parts are shadcn's, and its rows are the same rows `DropdownMenu` and `ContextMenu` publish.
12
+
13
+ ## Basic
14
+
15
+ ```tsx
16
+ import {
17
+ Menubar,
18
+ MenubarContent,
19
+ MenubarItem,
20
+ MenubarMenu,
21
+ MenubarSeparator,
22
+ MenubarShortcut,
23
+ MenubarTrigger,
24
+ } from '@oxyhq/bloom/menubar';
25
+
26
+ <Menubar>
27
+ <MenubarMenu value="file">
28
+ <MenubarTrigger>File</MenubarTrigger>
29
+ <MenubarContent>
30
+ <MenubarItem trailing={<MenubarShortcut>⌘N</MenubarShortcut>} onPress={newTab}>
31
+ New tab
32
+ </MenubarItem>
33
+ <MenubarItem onPress={open}>Open…</MenubarItem>
34
+ <MenubarSeparator />
35
+ <MenubarItem variant="destructive" onPress={closeWindow}>
36
+ Close window
37
+ </MenubarItem>
38
+ </MenubarContent>
39
+ </MenubarMenu>
40
+
41
+ <MenubarMenu value="edit">
42
+ <MenubarTrigger>Edit</MenubarTrigger>
43
+ <MenubarContent>
44
+ <MenubarItem onPress={undo}>Undo</MenubarItem>
45
+ <MenubarItem onPress={redo}>Redo</MenubarItem>
46
+ </MenubarContent>
47
+ </MenubarMenu>
48
+ </Menubar>
49
+ ```
50
+
51
+ Each `MenubarMenu` needs a `value`. That value IS the open state.
52
+
53
+ ## One open menu, one state
54
+
55
+ `Menubar` owns which menu is open, and each `MenubarMenu` derives its own boolean from it:
56
+
57
+ ```tsx
58
+ const [open, setOpen] = useState<string | undefined>(undefined);
59
+
60
+ <Menubar value={open} onValueChange={setOpen}>
61
+ ```
62
+
63
+ Uncontrolled by default (`defaultValue` sets the initially-open menu; omit it for none). Switching menus is a single state write, so there is never a frame with both open or neither.
64
+
65
+ ## Presentation
66
+
67
+ `MenubarTrigger` draws its own chrome — `rounded-md px-2 py-1.5` with a `text-sm font-medium` label, washed with the accent colour while its menu is open. Pass a string and it is set for you; pass nodes and they go inside that button; pass `asChild` and your own element replaces the whole thing.
68
+
69
+ On web each menu is an anchored panel under its own trigger, aligned to the trigger's left edge. On native each is the bottom sheet the other menu families use — a phone has no room for a hanging panel, and every Bloom overlay resolving to the same sheet is what makes the native surfaces feel like one system.
70
+
71
+ Pressing an open menu's trigger closes it.
72
+
73
+ ## Rows
74
+
75
+ `MenubarItem`, `MenubarCheckboxItem`, `MenubarRadioGroup` / `MenubarRadioItem`, `MenubarLabel`, `MenubarSeparator`, `MenubarShortcut`, `MenubarGroup`, and `MenubarSub` / `MenubarSubTrigger` / `MenubarSubContent`.
76
+
77
+ Sub-menus fly out beside their trigger row on web and render as an inline disclosure on native — see [DropdownMenu](/docs/bloom/main/dropdown-menu).
78
+
79
+ ## What is not here yet
80
+
81
+ Two things a desktop menu bar has that this one does not, both deliberate rather than forgotten:
82
+
83
+ - **Hover to switch.** On a real desktop bar, once one menu is open, moving the pointer across a sibling trigger switches to it without a click. That needs hover intent, which is the same machinery a sub-menu flyout needs.
84
+ - **Arrow-key navigation between menus.** Roving focus across the bar and into the open panel.
85
+
86
+ A press opens, a press closes, and every row is reachable by tab. Nothing is half-wired.