@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/card.mdx ADDED
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Card
3
+ description: The one card-shaped surface — background, border, elevation and corner decided in a single place.
4
+ order: 20
5
+ ---
6
+
7
+ # Card
8
+
9
+ `Card` is the answer to "what is a card made of". One component owns the `card`
10
+ background role, the border colour and width, the elevation and the corner
11
+ rung — and the platform split those imply, since a shadow is a `box-shadow` on
12
+ web and `elevation` on Android.
13
+
14
+ Five families used to draw that chrome by hand, with five different radii and
15
+ three different elevations. That is what `Card` removes. It does **not** remove
16
+ the variety: a settings group, a hover card and a link preview are legitimately
17
+ different surfaces, and the radius scale has rungs precisely so they can differ.
18
+
19
+ ```tsx
20
+ import { Card, CardHeader, CardBody, CardFooter, CardTitle, CardDescription } from '@oxyhq/bloom/card';
21
+
22
+ <Card variant="outlined" radius="radius-16">
23
+ <CardHeader>
24
+ <CardTitle>Weekly digest</CardTitle>
25
+ <CardDescription>Sent every Monday at 09:00.</CardDescription>
26
+ </CardHeader>
27
+ <CardBody>…</CardBody>
28
+ </Card>
29
+ ```
30
+
31
+ ## Which prop decides what
32
+
33
+ | prop | decides |
34
+ | ----------- | ---------------------------------------------------------------- |
35
+ | `variant` | the preset combination of background, border and elevation |
36
+ | `radius` | the corner, as a **rung of the `RADIUS` scale** — never a number |
37
+ | `border` | `none` · `hairline` (0.5px) · `thin` (1px) |
38
+ | `elevation` | `none` · `s` (a subtle raise) · `m` (the overlay role) |
39
+
40
+ `variant` is only a named default for the other three. An explicit `border` or
41
+ `elevation` wins, in both directions — you can add a shadow to `plain` and take
42
+ one away from `elevated`. That is deliberate: a surface that is "an outlined
43
+ card but flatter" should refine the axes rather than earn a fifth variant name.
44
+
45
+ ```tsx
46
+ <Card variant="plain" border="hairline" elevation="m" radius="radius-20" />
47
+ ```
48
+
49
+ ## Variants
50
+
51
+ - `plain` — the bare `card` surface: no border, no shadow.
52
+ - `elevated` — `shadow-s`. The default.
53
+ - `outlined` — a 1px border in the `border` role.
54
+ - `filled` — paints from `backgroundSecondary` instead of `card`, for a surface
55
+ that sits **on** a card rather than being one.
56
+
57
+ ## `radius` is a rung, not a number
58
+
59
+ ```tsx
60
+ <Card radius="radius-20" /> // ✅
61
+ <Card radius={20} /> // ✗ does not type-check
62
+ ```
63
+
64
+ The scale has 2 / 4 / 12 / 16 / 20 / 24 / 28 / max. Taking a rung rather than a
65
+ number is what keeps "this surface is a bit rounder" to eight values instead of
66
+ eight hundred, and it is why a design change to a rung moves every surface that
67
+ uses it.
68
+
69
+ ## The mistake it prevents
70
+
71
+ Hand-rolling the chrome means hand-rolling the platform split too, and the
72
+ usual result is a `Platform.OS` branch with slightly different numbers from the
73
+ one next to it. Worse, on web it is tempting to reach for classes —
74
+ `border border-border bg-card` — which are **inert until the consumer wires the
75
+ Tailwind pipeline**. That failure is silent: the card renders as an unbordered
76
+ transparent block, nothing throws, and it looks like a spacing bug.
77
+
78
+ `Card` applies background, border, radius and shadow as resolved inline tokens,
79
+ so the surface paints with or without a consumer's CSS pipeline.
80
+
81
+ ## Pressable cards
82
+
83
+ Passing `onPress` makes the card a `Pressable` with a press dip. Say what it
84
+ is: a card that opens a URL is a `link`, a card that performs an in-app action
85
+ is a `button` (the default).
86
+
87
+ ```tsx
88
+ <Card onPress={open} accessibilityRole="link" accessibilityLabel="Read the post" />
89
+ ```
90
+
91
+ ## Clipping
92
+
93
+ `Card` sets `overflow: 'hidden'` so a cover image cannot paint over the corner.
94
+ A surface that must *not* clip — because it has an Android elevation, or
95
+ because a child deliberately overflows — opts out through `style`:
96
+
97
+ ```tsx
98
+ <Card style={{ overflow: 'visible' }} />
99
+ ```
100
+
101
+ ## Parts
102
+
103
+ `CardHeader`, `CardBody`, `CardFooter`, `CardTitle` and `CardDescription` are
104
+ spacing and type conventions, not requirements. A card with entirely custom
105
+ content is a normal thing; use the parts when you want the same rhythm as every
106
+ other card.
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Checkbox
3
+ description: A tri-state checkbox with an optional label and description.
4
+ order: 24
5
+ ---
6
+
7
+ # Checkbox
8
+
9
+ A checkbox. Controlled, with an optional label and description, three sizes and a mixed state.
10
+
11
+ ## Basic
12
+
13
+ ```tsx
14
+ import { Checkbox } from '@oxyhq/bloom/checkbox';
15
+
16
+ const [checked, setChecked] = useState(false);
17
+
18
+ <Checkbox checked={checked} onCheckedChange={setChecked} label="Remember me" />
19
+ ```
20
+
21
+ `label` also becomes the accessible name, so a labelled checkbox needs nothing else. A checkbox with no label needs `accessibilityLabel`.
22
+
23
+ ```tsx
24
+ <Checkbox
25
+ checked={notify}
26
+ onCheckedChange={setNotify}
27
+ label="Email notifications"
28
+ description="Get a message when someone replies to you."
29
+ />
30
+ ```
31
+
32
+ ## The mixed state
33
+
34
+ ```tsx
35
+ <Checkbox
36
+ checked={allChecked}
37
+ indeterminate={!allChecked && someChecked}
38
+ onCheckedChange={(next) => setAll(next)}
39
+ label="Select all"
40
+ />
41
+ ```
42
+
43
+ `indeterminate` announces as `aria-checked="mixed"` and draws a rounded bar rather than a second glyph — the icon set has no minus, and a rule is what the state means.
44
+
45
+ ## Sizes and colour
46
+
47
+ `size` is `small`, `medium` (default) or `large`. `color` overrides the checked fill; it defaults to the theme primary, and the mark on top of it resolves to that preset's readable foreground rather than a hardcoded white.
48
+
49
+ ## Touch target
50
+
51
+ The box is 22dp at the default size, which is half of what a finger needs. The pressable is grown to 44dp with `hitSlop` — the target grows, the drawing does not — and the slack is derived per size, so `large` does not overshoot into a neighbouring row.
52
+
53
+ For a labelled checkbox the pressable already spans the label, so the whole row is the target.
54
+
55
+ ## Keyboard focus on web
56
+
57
+ The pressable takes a `:focus-visible` ring, drawn from the same shared recipe every Bloom control's web CSS is built from. It rings the element the browser actually focuses — the box AND its label — because ringing anything else would point at something that does not have focus.
58
+
59
+ `:focus-visible`, not `:focus`: a mouse click never leaves a ring behind.
package/docs/chip.mdx ADDED
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Chip
3
+ description: A labelled pill — a filter, a tag, a removable token.
4
+ order: 23
5
+ ---
6
+
7
+ # Chip
8
+
9
+ A labelled pill. A filter you can toggle, a tag you can remove, or a static token.
10
+
11
+ `Chip` is what shadcn calls `Badge`. Bloom's own [`Badge`](/docs/bloom/main/design-tokens) is a different component — a counter or a dot that rides on another control — so if you are porting a `<Badge>Draft</Badge>` from shadcn, this is the one you want.
12
+
13
+ ## Basic
14
+
15
+ ```tsx
16
+ import { Chip } from '@oxyhq/bloom/chip';
17
+
18
+ <Chip>Draft</Chip>
19
+ <Chip variant="solid" color="success">Published</Chip>
20
+ <Chip variant="subtle" color="warning">Needs review</Chip>
21
+ ```
22
+
23
+ ## Tone and fill
24
+
25
+ `color` picks the tone (`default`, `primary`, `success`, `warning`, `error`, `info`) and `variant` picks how loudly it is painted (`solid`, `subtle`, `outlined` — default `outlined`). shadcn's four badge variants map onto that pair: `default` is `solid` + `primary`, `secondary` is `subtle` + `default`, `destructive` is `solid` + `error`, and `outline` is `outlined` + `default`.
26
+
27
+ Both come from `theme/accent-colors.ts`, the one resolver `Chip` and `Badge` share. Never derive a tint yourself by appending alpha to a token — an accent resolves to `rgb(...)`, so the appended-hex form parses back fully opaque and paints the label on its own colour at contrast 1.00.
28
+
29
+ ## Filter pills
30
+
31
+ ```tsx
32
+ <Chip variant="subtle" selected={active} onPress={toggle}>
33
+ Music
34
+ </Chip>
35
+ ```
36
+
37
+ `selected` promotes the chip to the brand tone rather than to a colour system of its own, and announces as `aria-pressed`. On web a keyboard user gets a `:focus-visible` ring — keyboard-only, so a mouse click does not leave one behind.
38
+
39
+ ## Removable
40
+
41
+ ```tsx
42
+ <Chip onClose={() => removeTag(tag)}>{tag}</Chip>
43
+ ```
44
+
45
+ The close affordance is its own control inside the pill, with its own label and its own hit slop.
46
+
47
+ ## Sizes and icons
48
+
49
+ `size` is `small`, `medium` (default) or `large`. `startIcon` and `endIcon` take any node, and both are sized to the pill rather than to whatever was passed — an icon in a `small` chip and the same icon in a `large` one come out proportional.
50
+
51
+ ## A pill does not shrink
52
+
53
+ In a flex row of chips — a filter bar, a tag list — each pill keeps its size and the row overflows, rather than every label squeezing into an ellipsis to make room. Inside one pill, the label yields first: a chip narrower than its own text ellipsises rather than spilling past its radius.
package/docs/code.mdx ADDED
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: Code
3
+ description: Inline monospace and code blocks, as real elements rather than styled text.
4
+ order: 53
5
+ ---
6
+
7
+ # Code
8
+
9
+ `Code` is **inline** — an identifier, a flag, a token inside a sentence.
10
+ `Pre` is a **block** — a command, a snippet, a config file.
11
+
12
+ ```tsx
13
+ import { Code, Pre } from '@oxyhq/bloom/code';
14
+
15
+ <Text>
16
+ Pass <Code>variant="outlined"</Code> to draw the border.
17
+ </Text>
18
+
19
+ <Pre>{`bun add @oxyhq/bloom
20
+ bun run build`}</Pre>
21
+ ```
22
+
23
+ ## They are elements, not fonts
24
+
25
+ On web `Code` renders a real `<code>`. That matters for more than styling:
26
+ find-in-page, copy behaviour, and assistive technology all treat it as code
27
+ rather than as decoration. On native it is a `Text` in the monospace family.
28
+
29
+ ## `Pre` keeps its whitespace and scrolls
30
+
31
+ A block preserves line breaks and indentation and scrolls horizontally rather
32
+ than wrapping. Wrapping is the wrong default for a command: a wrapped command
33
+ is a command someone will copy wrong, and the break is invisible once pasted.
34
+
35
+ Write the content as a template literal so the source's own indentation does
36
+ not leak into the block.
37
+
38
+ ## Use `Code` for things that are typed, not for emphasis
39
+
40
+ `Code` means *this is a literal you would type or paste*: a prop name, a flag,
41
+ a file path, an env var. It is not a way to make a word stand out — monospace
42
+ in the middle of a sentence tells the reader "this is exact", and using it for
43
+ emphasis makes the real literals unfindable.
44
+
45
+ ## No syntax highlighting
46
+
47
+ `Pre` renders text. Highlighting means shipping a grammar for every language a
48
+ consumer might use, and getting it wrong is worse than not having it —
49
+ mis-tokenised code reads as broken code. If a surface genuinely needs
50
+ highlighting, it belongs to that surface.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Combobox
3
+ description: A select you can type into — for when the option list is too long to scan.
4
+ order: 42
5
+ ---
6
+
7
+ # Combobox
8
+
9
+ A picker with a search field. Reach for it when there are more options than a
10
+ user can scan — a country, a repository, a person.
11
+
12
+ ```tsx
13
+ import { Combobox } from '@oxyhq/bloom/combobox';
14
+
15
+ <Combobox
16
+ options={apps.map((a) => ({ value: a.id, label: a.name, description: a.tagline }))}
17
+ value={appId}
18
+ onValueChange={setAppId}
19
+ placeholder="Pick an app"
20
+ emptyText="No apps match that"
21
+ />
22
+ ```
23
+
24
+ ## Which prop decides what
25
+
26
+ | prop | decides |
27
+ | ------------------------- | ---------------------------------------------------- |
28
+ | `options` | the choices — `value`, `label`, optional `description` |
29
+ | `value` / `onValueChange` | the selection; `null` means nothing chosen |
30
+ | `placeholder` | the trigger text while nothing is selected |
31
+ | `emptyText` | what an empty result says |
32
+ | `filter` | replaces the default match |
33
+ | `query` / `onQueryChange` | the search text, if you want to control it |
34
+ | `maxListHeight` | how tall the list may grow |
35
+
36
+ ## The trigger shows the LABEL
37
+
38
+ `value` is an opaque id to the user. A combobox that renders the value into the
39
+ field shows `usr_8f21c` where the user chose "Ada Lovelace" — which reads as a
40
+ bug rather than as a raw id.
41
+
42
+ ## Controlled `query` is for search that leaves the component
43
+
44
+ By default the combobox owns the query. Take control of it when the *filtering*
45
+ happens elsewhere — a debounced server search, a query mirrored into the URL:
46
+
47
+ ```tsx
48
+ <Combobox query={q} onQueryChange={setQ} options={serverResults} filter={() => true} />
49
+ ```
50
+
51
+ Note the `filter={() => true}`: if the server already filtered, filtering again
52
+ locally throws away results that matched for reasons the client cannot see.
53
+
54
+ ## `emptyText` earns its place
55
+
56
+ An empty popover reads as broken. Saying "No apps match that" tells the user
57
+ the search worked and there is nothing — a different message from silence.
58
+
59
+ ## Combobox, Select or Command
60
+
61
+ - **Combobox** — many values, typing needed to find one, selection persists.
62
+ - **[Select](/docs/bloom/main/select)** — few enough values to scan.
63
+ - **[Command](/docs/bloom/main/command)** — **actions**, not values; runs one
64
+ and closes.
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Command
3
+ description: A command palette — search across everything the user could do from here.
4
+ order: 41
5
+ ---
6
+
7
+ # Command
8
+
9
+ A searchable list of actions, opened over the page. Type to filter, press to run.
10
+
11
+ ```tsx
12
+ import { Command } from '@oxyhq/bloom/command';
13
+
14
+ <Command
15
+ visible={paletteOpen}
16
+ onClose={() => setPaletteOpen(false)}
17
+ placeholder="Type a command…"
18
+ items={[
19
+ {
20
+ id: 'new-post',
21
+ label: 'New post',
22
+ group: 'Create',
23
+ shortcut: '⌘N',
24
+ icon: PencilIcon,
25
+ onSelect: newPost,
26
+ },
27
+ {
28
+ id: 'settings',
29
+ label: 'Open settings',
30
+ description: 'Account, appearance, notifications',
31
+ keywords: ['preferences', 'account'],
32
+ group: 'Navigate',
33
+ onSelect: () => router.push('/settings'),
34
+ },
35
+ ]}
36
+ />
37
+ ```
38
+
39
+ ## Which prop decides what
40
+
41
+ | prop | decides |
42
+ | --------------- | ----------------------------------------------------------- |
43
+ | `visible` / `onClose` | whether it is open — controlled |
44
+ | `items` | everything that can be run |
45
+ | `query` / `onQueryChange` | the search text, if you want to control it |
46
+ | `filter` | replaces the default match |
47
+ | `emptyText` | what an empty result says |
48
+ | `maxListHeight` | how tall the list may grow |
49
+
50
+ ## `keywords` is what makes a palette usable
51
+
52
+ A palette is only as good as its synonyms. A user looking for "preferences"
53
+ will not type "settings", and a user looking for "dark mode" will not type
54
+ "appearance". `keywords` are matched alongside the label and description and
55
+ are never shown, so they cost nothing on screen.
56
+
57
+ ```tsx
58
+ { id: 'theme', label: 'Appearance', keywords: ['dark mode', 'light', 'theme'] }
59
+ ```
60
+
61
+ ## `group` is a heading, not a filter
62
+
63
+ Items are grouped under their `group` label in the order the groups first
64
+ appear. Grouping helps a user *scan* a list they did not search; once they type,
65
+ what matters is the match, not the category.
66
+
67
+ ## `shortcut` is a reminder, not a binding
68
+
69
+ Setting it draws the key hint on the row. It does **not** register a keyboard
70
+ shortcut — the app owns its key handling, and a palette that silently bound
71
+ keys would collide with the page underneath.
72
+
73
+ ## An item that cannot run should not be listed
74
+
75
+ Prefer omitting an inapplicable action to showing it `disabled`. A palette is
76
+ searched, not browsed: a disabled row is a match that answers the query and
77
+ then refuses, which is worse than no match at all. Keep `disabled` for the case
78
+ where the user needs to know the action *exists* here but is unavailable right
79
+ now.
80
+
81
+ ## Command or Combobox
82
+
83
+ - **Command** — a list of **actions**, opened over the page, closed after one.
84
+ - **[Combobox](/docs/bloom/main/combobox)** — a list of **values**, inside a
85
+ form, producing a selection that stays.
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: CompositionBar
3
+ description: What a total is made of — a segmented bar with a readout you can interrogate.
4
+ order: 62
5
+ ---
6
+
7
+ # CompositionBar
8
+
9
+ A single bar split into coloured segments, with a readout for the selected one.
10
+ Use it when the question is *what is this total made of* — a portfolio by
11
+ asset, spending by category, storage by file type.
12
+
13
+ ```tsx
14
+ import { CompositionBar } from '@oxyhq/bloom/composition-bar';
15
+
16
+ <CompositionBar
17
+ categories={[
18
+ { key: 'eth', name: 'Ethereum', amount: 4200, color: '#627EEA' },
19
+ { key: 'btc', name: 'Bitcoin', amount: 3100, color: '#F7931A' },
20
+ { key: 'other', name: 'Other', amount: 900, color: '#9CA3AF' },
21
+ ]}
22
+ selectedKey={selected}
23
+ onSelect={setSelected}
24
+ hintLabel="Tap a segment"
25
+ />
26
+ ```
27
+
28
+ ## Which prop decides what
29
+
30
+ | prop | decides |
31
+ | ---------------- | --------------------------------------------------------- |
32
+ | `categories` | the segments — `key`, `name`, `amount`, `color` |
33
+ | `selectedKey` | which segment's readout is shown; `null` for none |
34
+ | `onSelect` | the press |
35
+ | `hintLabel` | the prompt shown while nothing is selected |
36
+ | `formatReadout` | how the selected amount and percentage are worded |
37
+
38
+ ## The readout is the point
39
+
40
+ `selectedKey` and `onSelect` are required. A segmented bar with no way to ask
41
+ "which slice is that?" is decoration: the colours are unlabelled and the widths
42
+ are unreadable below about 5%.
43
+
44
+ `null` is a real state — nothing selected — and not the same as selecting the
45
+ first category on the user's behalf. `hintLabel` is what fills that space.
46
+
47
+ ## Amounts, not percentages
48
+
49
+ Pass raw `amount`s and let the component compute the shares. Pre-computing
50
+ percentages is how the segments stop summing to 100 after rounding, which shows
51
+ up as a sliver of background at the end of the bar.
52
+
53
+ ## Keep the category count low
54
+
55
+ Five or six. Beyond that the small segments are too thin to press and the
56
+ colours stop being distinguishable — group the tail into an "Other" category
57
+ with its own neutral colour, as above.
58
+
59
+ ## `formatReadout`
60
+
61
+ The default states the amount and its share. Override it when the unit needs
62
+ saying (`$4,200`, `4.2 GB`) — a bare number in a readout is the same problem as
63
+ a bare number anywhere else.
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: ConnectionDots
3
+ description: An animated link between two things — an OAuth consent shape, not a loading state.
4
+ order: 68
5
+ ---
6
+
7
+ # ConnectionDots
8
+
9
+ Two elements with a run of animated dots travelling between them. It is the
10
+ "this app is connecting to your account" illustration on a consent screen.
11
+
12
+ ```tsx
13
+ import { ConnectionDots } from '@oxyhq/bloom/connection-dots';
14
+
15
+ <ConnectionDots
16
+ left={<AppLogo />}
17
+ right={<Avatar source={user.avatar} size={48} />}
18
+ accessibilityLabel="Notion is connecting to your Oxy account"
19
+ />
20
+ ```
21
+
22
+ ## Which prop decides what
23
+
24
+ | prop | decides |
25
+ | -------------------- | --------------------------------------------------- |
26
+ | `left` / `right` | the two endpoints — required |
27
+ | `dotCount` / `dotSize` | the run between them |
28
+ | `accessibilityLabel` | what the whole illustration means |
29
+ | `reducedMotion` | render the dots static instead of animating |
30
+
31
+ ## It illustrates a relationship, not progress
32
+
33
+ The dots move continuously; they do not fill up. Nothing here says how far
34
+ along anything is, and using it while an operation runs implies a progress bar
35
+ that never advances. For work in flight use [`Loading`](/docs/bloom/main/loading).
36
+
37
+ ## Give it a label
38
+
39
+ The whole point is carried by the picture: two parties and a link between them.
40
+ Without `accessibilityLabel` a screen reader gets an app logo, an avatar, and
41
+ nothing about the relationship — which is exactly the thing the user is being
42
+ asked to consent to.
43
+
44
+ Name both sides in the label, in the direction the consent runs.
45
+
46
+ ## `reducedMotion`
47
+
48
+ Honour the OS setting. Perpetual motion is one of the more uncomfortable
49
+ patterns for motion-sensitive users, and this one runs for as long as the
50
+ screen is open. The static form still communicates the link.
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: ConnectionStatusToasts
3
+ description: Offline and reconnected, announced once — with a delay that hides the flap.
4
+ order: 70
5
+ ---
6
+
7
+ # ConnectionStatusToasts
8
+
9
+ Mount it once and the app announces losing and regaining its connection.
10
+
11
+ ```tsx
12
+ import { ConnectionStatusToasts } from '@oxyhq/bloom/connection-status';
13
+
14
+ <ConnectionStatusToasts />
15
+ ```
16
+
17
+ ## Which prop decides what
18
+
19
+ | prop | decides |
20
+ | ---------------------- | ----------------------------------------------------- |
21
+ | `offlineMessage` | shown while there is no connection |
22
+ | `reconnectingMessage` | shown after `reconnectingDelayMs` offline |
23
+ | `restoredMessage` | shown when the connection comes back |
24
+ | `reconnectingDelayMs` | how long to wait before escalating |
25
+ | `restoredDurationMs` | how long the restored toast stays |
26
+
27
+ ## "Back online" is only shown to someone who saw the outage
28
+
29
+ The restored toast is suppressed unless an outage toast was actually up. A
30
+ "Back online" that appears on its own is confusing — the user never knew
31
+ anything was wrong, and the message implies they missed something.
32
+
33
+ ## The delay is the point
34
+
35
+ Connectivity flaps. Announcing every dropped packet produces a toast that
36
+ appears and disappears while the user is reading, which is worse than silence.
37
+ The offline toast is shown immediately because the user is about to hit a
38
+ failure; the *reconnecting* escalation waits, so a two-second blip never
39
+ escalates at all.
40
+
41
+ ## Optional peer
42
+
43
+ Connectivity comes from `@react-native-community/netinfo`, loaded through
44
+ Bloom's optional-require boundary. Without the package installed the component
45
+ mounts and does nothing rather than failing the build.
46
+
47
+ That is why the require is written the way it is — a literal specifier, as a
48
+ direct statement of a `try` block. Metro's optionality walk returns at the
49
+ first enclosing block, so one `if` of nesting inside the `try` would lose
50
+ optionality and turn a missing optional peer into a build failure.
51
+
52
+ ## Mount it once
53
+
54
+ It presents through the shared [toast](/docs/bloom/main/toast) engine with a
55
+ fixed id, so a second mount does not duplicate the toast — but it does double
56
+ the listeners and the timers. One per app.
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: ContentPanel
3
+ description: The reading column — a width constraint that optionally becomes a framed surface.
4
+ order: 50
5
+ ---
6
+
7
+ # ContentPanel
8
+
9
+ The column a screen's content lives in. It caps the reading width and, above a
10
+ breakpoint, decides whether that column reads as a framed surface or as the
11
+ page itself.
12
+
13
+ ```tsx
14
+ import { ContentPanel } from '@oxyhq/bloom/content-panel';
15
+
16
+ <ContentPanel framed framedFrom={640}>
17
+ <SettingsScreen />
18
+ </ContentPanel>
19
+ ```
20
+
21
+ ## Which prop decides what
22
+
23
+ | prop | decides |
24
+ | ------------------ | --------------------------------------------------------- |
25
+ | `framed` | whether the panel draws a surface at all |
26
+ | `framedFrom` | the breakpoint it starts framing at — `500` `640` `768` `1024` |
27
+ | `showStickyFrame` | whether the frame persists behind sticky content |
28
+ | `surfaceStyle` / `surfaceClassName` | the surface itself |
29
+ | `contentStyle` / `contentClassName` | the column inside it |
30
+ | `maskColor` | the colour the gutter mask fades to |
31
+
32
+ ## `framedFrom` is why the branch is not in your screen
33
+
34
+ A phone wants a plain column: a frame a few pixels from the screen edge reads
35
+ as a rendering bug, not as a card. A desktop wants the framed surface. Handling
36
+ that in the screen means every screen re-derives the same breakpoint, and they
37
+ drift.
38
+
39
+ ## Panels are siblings, never nested
40
+
41
+ `ContentPanel` **throws** if you nest one inside another. That is deliberate: a
42
+ panel in a panel would frame inside a frame and inset the reading column twice,
43
+ which looks merely "a bit padded" and would survive review indefinitely. A loud
44
+ failure in development is the cheaper outcome.
45
+
46
+ Two columns side by side are two panels as **siblings**:
47
+
48
+ ```tsx
49
+ <View style={{ flexDirection: 'row', gap: 16 }}>
50
+ <View style={{ flex: 2 }}><ContentPanel framed>…</ContentPanel></View>
51
+ <View style={{ flex: 1 }}><ContentPanel framed>…</ContentPanel></View>
52
+ </View>
53
+ ```
54
+
55
+ A screen picks one panel per column; the sections inside it are plain views.
56
+
57
+ ## Do not put a Card immediately inside a framed panel
58
+
59
+ They are both surfaces. A card filling a framed panel is two borders with a few
60
+ pixels between them. Use the panel's frame, and reach for
61
+ [`Card`](/docs/bloom/main/card) for things *within* the column that are
62
+ genuinely separate objects.