@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/tabs.mdx ADDED
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Tabs
3
+ description: Switching which panel is shown, controlled from the outside.
4
+ order: 26
5
+ ---
6
+
7
+ # Tabs
8
+
9
+ `Tabs` is the **strip**, and only the strip: the triggers, the sliding
10
+ indicator and the horizontal scroll. It is controlled — `value` is the open tab
11
+ and `onValueChange` reports a press — and the caller renders the panel for
12
+ whatever `value` currently is.
13
+
14
+ ```tsx
15
+ import { Tabs, TabsTrigger } from '@oxyhq/bloom/tabs';
16
+
17
+ <Tabs value={tab} onValueChange={setTab}>
18
+ <TabsTrigger value="posts" label="Posts" />
19
+ <TabsTrigger value="replies" label="Replies" />
20
+ <TabsTrigger value="media" label="Media" count={12} />
21
+ </Tabs>
22
+
23
+ {tab === 'posts' ? <Posts /> : null}
24
+ {tab === 'replies' ? <Replies /> : null}
25
+ {tab === 'media' ? <Media /> : null}
26
+ ```
27
+
28
+ Keeping the panels outside is what lets a screen put the strip in a sticky
29
+ header, in a navigator, or beside the content — the strip does not have to be
30
+ an ancestor of what it switches.
31
+
32
+ ## Which prop decides what
33
+
34
+ | prop | decides |
35
+ | --------------- | ------------------------------------------------------------- |
36
+ | `value` | the open tab |
37
+ | `onValueChange` | receives the pressed trigger's `value` |
38
+ | `hasSelection` | whether **any** tab is selected — see below |
39
+ | `variant` | `underline` · `filled` · `outlined` |
40
+ | `fullWidth` | spread the triggers across the container instead of hugging |
41
+ | `count` | a number beside a trigger's label |
42
+
43
+ ## `hasSelection` exists for a state a tab strip usually cannot express
44
+
45
+ A filter row where nothing is chosen yet. Without it, the first tab looks
46
+ selected before the user has selected anything, and "All" and "no filter" are
47
+ indistinguishable.
48
+
49
+ ```tsx
50
+ <Tabs value={filter} hasSelection={filter != null} onValueChange={setFilter}>
51
+ ```
52
+
53
+ ## `count` is a quantity you are choosing between
54
+
55
+ 3 drafts versus 41 sent — a number that helps you decide which tab to open. It
56
+ is not an unread badge; unread belongs on the thing that is unread, through
57
+ [`Badge`](/docs/bloom/main/badge), where `invisible` keeps the layout still as
58
+ the count reaches zero.
59
+
60
+ ## Tabs, SegmentedControl or a Select
61
+
62
+ - **Tabs** — several views of one subject, with real content underneath.
63
+ - **[SegmentedControl](/docs/bloom/main/segmented-control)** — the choice *is*
64
+ the value (a setting). Announces as a radio group.
65
+ - **[Select](/docs/bloom/main/select)** — too many options to show at once, or
66
+ the options are data rather than fixed sections.
67
+
68
+ The first two look similar enough that the decision is worth making explicitly:
69
+ after the user picks, has something been *set*, or is the same thing now shown
70
+ differently?
71
+
72
+ ## expo-router
73
+
74
+ `@oxyhq/bloom/tabs/expo-router` binds the strip to the router so `value` comes
75
+ from the current route and a press navigates, instead of a local `useState`.
@@ -0,0 +1,187 @@
1
+ ---
2
+ title: TextField
3
+ description: Composable text input — the field, its input, label, icon, suffix and ghost.
4
+ order: 11
5
+ ---
6
+
7
+ # TextField
8
+
9
+ A composable text input. `TextField` is the field itself; put a `TextFieldInput` inside it, plus any of `TextFieldIcon`, `TextFieldSuffix` and `TextFieldGhost`. The field tracks focus, hover and invalid state and hands it to those parts, so the whole chrome reacts as one.
10
+
11
+ ## Basic
12
+
13
+ ```tsx
14
+ import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
15
+
16
+ <TextField>
17
+ <TextFieldInput
18
+ label="Email"
19
+ value={value}
20
+ onChangeText={setValue}
21
+ keyboardType="email-address"
22
+ />
23
+ </TextField>
24
+ ```
25
+
26
+ `label` is required and is the input's accessible name. Without an explicit `placeholder` it doubles as the placeholder; pass `placeholder={null}` for none.
27
+
28
+ ## One input needs no wrapper
29
+
30
+ ```tsx
31
+ <TextFieldInput label="Email" value={value} onChangeText={setValue} />
32
+ ```
33
+
34
+ `TextFieldInput` on its own wraps itself in a `TextField`, so the common case is one element. Reach for the explicit `TextField` when you have more than one part to put in it, or when you need `isInvalid` or `radius` on the field.
35
+
36
+ ## With an icon
37
+
38
+ ```tsx
39
+ <TextField>
40
+ <TextFieldIcon icon={Envelope_Stroke2_Corner0_Rounded} />
41
+ <TextFieldInput label="Email" value={email} onChangeText={setEmail} />
42
+ </TextField>
43
+ ```
44
+
45
+ The field lays its children out in a row and focuses the input when you click anywhere in it (web).
46
+
47
+ ## Which parts need the field
48
+
49
+ `TextFieldIcon` and `TextFieldSuffix` read the field's focus, hover and invalid state, and **throw** when rendered outside a `TextField` — they have nothing to colour themselves against, and a part that renders anyway would look finished while never reacting to anything. The message names what is missing:
50
+
51
+ ```
52
+ TextField parts must be rendered inside a <TextField>.
53
+ ```
54
+
55
+ `TextFieldInput` does not throw: a missing field is how it knows to wrap itself. `TextFieldLabel` sits above the field by design, and `TextFieldGhost` reads no state — both render anywhere.
56
+
57
+ ## Validation
58
+
59
+ ```tsx
60
+ const [value, setValue] = useState('');
61
+ const [touched, setTouched] = useState(false);
62
+ const isInvalid = touched && !value.includes('@');
63
+
64
+ <TextField isInvalid={isInvalid}>
65
+ <TextFieldInput
66
+ label="Email"
67
+ value={value}
68
+ onChangeText={setValue}
69
+ onBlur={() => setTouched(true)}
70
+ />
71
+ </TextField>
72
+ {isInvalid ? <Text style={{ color: theme.colors.negative }}>Enter a valid email</Text> : null}
73
+ ```
74
+
75
+ `isInvalid` paints the field in the negative palette and reaches every part through context. `TextFieldInput` takes its own `isInvalid` too, for the wrapper-less spelling.
76
+
77
+ ## Floating label
78
+
79
+ ```tsx
80
+ <TextField>
81
+ <TextFieldInput floatingLabel label="Email" value={email} onChangeText={setEmail} />
82
+ </TextField>
83
+ ```
84
+
85
+ The label starts inside the field and animates to a caption at the top on focus or once there is a value. State drives it, not CSS, so web and native behave identically; reduced motion snaps instead of animating.
86
+
87
+ ## Multiline
88
+
89
+ ```tsx
90
+ <TextField style={{ minHeight: 96, alignItems: 'flex-start' }}>
91
+ <TextFieldInput label="Bio" value={bio} onChangeText={setBio} multiline />
92
+ </TextField>
93
+ ```
94
+
95
+ ## Suffix
96
+
97
+ ```tsx
98
+ <TextField>
99
+ <TextFieldInput label="Workspace" value={url} onChangeText={setUrl} />
100
+ <TextFieldSuffix label="domain">.oxy.so</TextFieldSuffix>
101
+ </TextField>
102
+ ```
103
+
104
+ ## Components
105
+
106
+ ### `TextField`
107
+
108
+ | Prop | Type | Description |
109
+ |------|------|-------------|
110
+ | `isInvalid?` | `boolean` | Paints the field in the negative palette and exposes it via context. |
111
+ | `radius?` | `number` | Corner radius of the chrome, default `10`. A large value (999) reads as a pill. |
112
+ | `style?` | `StyleProp<ViewStyle>` | Override the container style. |
113
+ | `children?` | `React.ReactNode` | The input, plus any icon / suffix / ghost. |
114
+
115
+ ### `TextFieldInput`
116
+
117
+ Accepts React Native's `TextInputProps`, plus:
118
+
119
+ | Prop | Type | Description |
120
+ |------|------|-------------|
121
+ | `label` | `string` | Required. The accessible name, and the default placeholder. |
122
+ | `value?` / `onChangeText?` | `string` / `(v: string) => void` | The controlled value. |
123
+ | `placeholder?` | `string \| null` | Overrides the label as placeholder; `null` for none. |
124
+ | `isInvalid?` | `boolean` | For the wrapper-less spelling. |
125
+ | `floatingLabel?` | `boolean` | Material-style animated label. Default `false`. |
126
+ | `inputRef?` | `RefObject<TextInput \| null>` | Merged with the field's own ref. |
127
+
128
+ ### `TextFieldIcon`
129
+
130
+ | Prop | Type | Description |
131
+ |------|------|-------------|
132
+ | `icon` | `React.ComponentType<SVGIconProps>` | A Bloom icon component. Takes the field's hover / focus / error colour. |
133
+
134
+ Renders before the input, so put it first.
135
+
136
+ ### `TextFieldLabel`
137
+
138
+ A visible label rendered ABOVE the field, outside it:
139
+
140
+ ```tsx
141
+ <TextFieldLabel>Workspace</TextFieldLabel>
142
+ <TextField>
143
+ <TextFieldInput label="Workspace" value={name} onChangeText={setName} />
144
+ </TextField>
145
+ ```
146
+
147
+ ### `TextFieldSuffix`
148
+
149
+ Inline trailing text — domain suffixes, units. `label` is its accessible name; `accessibilityHint` and `style` are optional.
150
+
151
+ ### `TextFieldGhost`
152
+
153
+ Greyed completion text drawn behind the input, for "type `acme`, see `acme.oxy.so`". Takes the typed prefix as `children` and the suggested remainder as `value`; it is `aria-hidden`.
154
+
155
+ ## Accessibility
156
+
157
+ - Clicking anywhere in the field focuses the input (web).
158
+ - `label` is the input's `accessibilityLabel`; the floating label itself is hidden from assistive tech so it is not announced twice.
159
+ - On web the browser's own focus ring is stripped, because it follows the input's box rather than the field's and pokes out of a pill's corners. The animated border IS the focus signal.
160
+ - Invalid state paints the chrome. Surface the reason as text under the field — colour alone is not an announcement.
161
+
162
+ ## Recipes
163
+
164
+ ### Password with visibility toggle
165
+
166
+ ```tsx
167
+ const [show, setShow] = useState(false);
168
+
169
+ <TextField>
170
+ <TextFieldInput
171
+ label="Password"
172
+ value={password}
173
+ onChangeText={setPassword}
174
+ secureTextEntry={!show}
175
+ />
176
+ <Button
177
+ variant="icon"
178
+ icon={show ? <EyeSlash_Stroke2_Corner0_Rounded /> : <Eye_Stroke2_Corner0_Rounded />}
179
+ accessibilityLabel={show ? 'Hide password' : 'Show password'}
180
+ onPress={() => setShow(!show)}
181
+ />
182
+ </TextField>
183
+ ```
184
+
185
+ ### Search
186
+
187
+ Reach for `Search` rather than rebuilding it: it is a `TextField` at the pill radius with the magnifier, the clear button and the keyboard settings a query field wants.
package/docs/theme.mdx ADDED
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Theme
3
+ description: Color presets, light/dark modes, and design tokens used by every Bloom component.
4
+ order: 2
5
+ ---
6
+
7
+ # Theme
8
+
9
+ Wrap your app with `BloomThemeProvider`. It accepts controlled `mode` and `colorPreset` props — persist them however you like (AsyncStorage, Zustand, etc.).
10
+
11
+ ```tsx
12
+ import { BloomThemeProvider } from '@oxyhq/bloom/theme';
13
+
14
+ <BloomThemeProvider mode="system" colorPreset="teal">
15
+ <App />
16
+ </BloomThemeProvider>
17
+ ```
18
+
19
+ ## Modes
20
+
21
+ | Mode | Behavior |
22
+ |------|----------|
23
+ | `light` | Force light theme. |
24
+ | `dark` | Force dark theme. |
25
+ | `system` | Follow OS preference (`useColorScheme`). |
26
+ | `adaptive` | iOS/Android native dynamic colors when available, falls back to `system`. |
27
+
28
+ ## Color presets
29
+
30
+ 10 named palettes:
31
+
32
+ `teal` · `blue` · `green` · `amber` · `red` · `purple` · `pink` · `sky` · `orange` · `mint`
33
+
34
+ Each preset has a tuned light + dark variant and a contrast-checked primary color.
35
+
36
+ ## Reading theme values
37
+
38
+ ```tsx
39
+ import { useTheme } from '@oxyhq/bloom/theme';
40
+
41
+ function MyComponent() {
42
+ const theme = useTheme();
43
+ return (
44
+ <View style={{ backgroundColor: theme.colors.surface }}>
45
+ <Text style={{ color: theme.colors.text }}>{theme.isDark ? 'Dark' : 'Light'}</Text>
46
+ </View>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ### Tokens
52
+
53
+ Every preset exposes:
54
+
55
+ - `colors.primary` — brand color.
56
+ - `colors.background` — page background.
57
+ - `colors.surface` — elevated surface (cards, sheets).
58
+ - `colors.text` — body text.
59
+ - `colors.muted` — secondary text.
60
+ - `colors.border` — divider lines.
61
+ - `colors.success` / `colors.warning` / `colors.danger` — semantic colors.
62
+ - `isDark` — boolean.
63
+ - `mode` — resolved mode (`'light' | 'dark'`).
64
+
65
+ <Callout variant="info" title="Web CSS variables">
66
+ On web, Bloom also writes the active palette to CSS variables on `:root`, so non-React surfaces (e.g. raw HTML pages) can theme themselves with `var(--bloom-primary)`.
67
+ </Callout>
package/docs/toast.mdx ADDED
@@ -0,0 +1,285 @@
1
+ ---
2
+ title: Toast
3
+ description: Passive notifications, powered by Bloom's own universal toast engine.
4
+ order: 5
5
+ ---
6
+
7
+ # Toast
8
+
9
+ Passive notifications. Bloom ships its own engine — **one** implementation that runs
10
+ on native and on web (through react-native-web), so there is nothing extra to
11
+ install and the API is identical everywhere.
12
+
13
+ The engine is derived from [sonner-native](https://github.com/gunnartorfis/sonner-native)
14
+ v0.26.4 (MIT), itself a React Native port of [sonner](https://github.com/emilkowalski/sonner).
15
+
16
+ ## Setup
17
+
18
+ Mount the outlet once near the app root, inside `BloomThemeProvider`:
19
+
20
+ ```tsx
21
+ import { ToastOutlet } from '@oxyhq/bloom/toast';
22
+
23
+ <BloomThemeProvider>
24
+ <App />
25
+ <ToastOutlet />
26
+ </BloomThemeProvider>
27
+ ```
28
+
29
+ Apps that mount `OxyProvider` from `@oxyhq/services` already get an outlet and
30
+ should **not** mount a second one — two outlets render every toast twice.
31
+
32
+ Native apps must wrap the root in `GestureHandlerRootView` (as they already do for
33
+ `Dialog` / `BottomSheet`) for swipe-to-dismiss. On web the toast host provides its
34
+ own, so no setup is needed.
35
+
36
+ ## API
37
+
38
+ ```tsx
39
+ import { toast } from '@oxyhq/bloom';
40
+
41
+ toast('Saved');
42
+ toast.success('Profile updated');
43
+ toast.error('Network error', { duration: 5000 });
44
+ toast.warning('Please verify your email');
45
+ toast.info('A new version is available');
46
+ toast.loading('Uploading…');
47
+
48
+ const id = toast('Uploading…');
49
+ toast.dismiss(id); // one toast
50
+ toast.dismiss(); // all of them
51
+ ```
52
+
53
+ Every method returns the toast's id (`string | number`), so `const id = toast(…)`
54
+ followed by `toast.dismiss(id)` works. Passing your own `id` updates that toast in
55
+ place instead of adding another:
56
+
57
+ ```tsx
58
+ toast.loading('Uploading…', { id: 'upload' });
59
+ toast.success('Uploaded', { id: 'upload' }); // replaces the row, keeps its position
60
+ ```
61
+
62
+ `toast(content, options?)` takes a string or a React element; anything else throws.
63
+ `options.type` is the public variant knob and the typed helpers above are shorthands
64
+ for it. `type: 'default'` (the default) renders a neutral toast — no icon, no tint.
65
+
66
+ ### The variant colours the icon, not the surface
67
+
68
+ Every toast renders the SAME neutral surface — `backgroundSecondary` with the
69
+ `border`, `text` and `textSecondary` roles — whatever its `type`, and with
70
+ `richColors` on or off. The variant shows up only in the leading icon, which uses
71
+ the theme's first-class status tokens: `success`, `error`, `warning`, `info`. A
72
+ `loading` or variant-less toast gets `textSecondary`.
73
+
74
+ That is deliberate, and matches sonner. `toast.success('Saved')` should read as a
75
+ normal toast with a green check, not as a full brand-coloured card.
76
+
77
+ `richColors` lifts the status colour from the icon alone to the **border and
78
+ title** as well. That is all it does: the surface stays `backgroundSecondary`, and
79
+ the description, close button and action colours stay neutral, so a rich-coloured
80
+ row still reads as a toast rather than a status banner. All four status variants
81
+ behave identically under it — `warning` and `info` are not special-cased and get
82
+ exactly the same border+title treatment as `success` and `error`, because the
83
+ lift is the only tinting mechanism there is. `loading` and a variant-less toast
84
+ have no status colour, so `richColors` has nothing to make more prominent and
85
+ they stay fully neutral either way.
86
+
87
+ <Callout variant="info" title="Tinted status surfaces need new tokens">
88
+ There is no `warningSubtle` / `infoSubtle` (or `successSubtle` / `errorSubtle`)
89
+ pair to tint a surface with, which is why every variant gets the same border+title
90
+ lift instead. Bloom has `*Subtle` / `*SubtleForeground` pairs for the **brand**
91
+ roles (`primarySubtle`, `negativeSubtle`) only — those are not status colours, and
92
+ reaching for them here is what once made a success toast render brand-purple.
93
+ Genuinely tinted status surfaces would need that missing status token family: a
94
+ theme addition affecting every app, not something to fake from a brand pair or a
95
+ derived alpha.
96
+ </Callout>
97
+
98
+ ### Recolouring toasts with `BloomColorScope`
99
+
100
+ `BloomColorScope` is a React context override, and React context flows through a
101
+ portal — so a scope **does** reach the toast even though the row renders into
102
+ `#bloom-portal-root` at the end of `<body>`. The catch is which side of the scope
103
+ the OUTLET is on, not which side the `toast()` call is on:
104
+
105
+ ```tsx
106
+ // ✅ tints toasts — the scope is an ancestor of the outlet
107
+ <BloomColorScope colorPreset="green">
108
+ <App />
109
+ <ToastOutlet />
110
+ </BloomColorScope>
111
+
112
+ // ❌ does NOT tint toasts — the outlet is outside the scope
113
+ <BloomColorScope colorPreset="green">
114
+ <SomeScreen /> {/* toast() called from in here */}
115
+ </BloomColorScope>
116
+ <ToastOutlet />
117
+ ```
118
+
119
+ **Apps that mount `OxyProvider` are in the second shape.** It renders
120
+ `<ToastOutlet />` at app root, so a `BloomColorScope` wrapped around a screen deep
121
+ in the tree will not tint toasts fired from that screen — they follow the root
122
+ preset. To recolour toasts, move the scope up so it wraps the provider.
123
+
124
+ What a scope changes is the **preset-derived** roles: the surface, text and
125
+ border. Status colours are fixed theme tokens, so a `success` icon (and, under
126
+ `richColors`, the border and title) is the same green whichever preset is in
127
+ scope — only the neutral card around it moves.
128
+
129
+ ### Width
130
+
131
+ A toast is a card, not a banner. The row is `width: 100%` up to a **388px cap**,
132
+ centred — which puts the visible card on 356px, sonner's reference width, plus its
133
+ two 16px gutters. Below a 388px viewport the cap never binds and the card stays
134
+ screen-width-minus-32, so phone portrait is unaffected; above it (desktop, tablet,
135
+ phone landscape) the card caps and centres instead of spanning the viewport.
136
+
137
+ Widen it per outlet through the existing style slot:
138
+
139
+ ```tsx
140
+ <ToastOutlet toastOptions={{ toastContainerStyle: { maxWidth: 600 } }} />
141
+ ```
142
+
143
+ That moves the card only. The swipe-to-dismiss threshold and the stacking depth
144
+ cue are fractions of the **default** cap, so a widened card keeps the default
145
+ gesture distances.
146
+
147
+ ### Stacking
148
+
149
+ Toasts **stack** by default, the way sonner's do: the newest row sits in front at
150
+ full size, older ones sit behind it scaled down and offset by `gap` (8px), and
151
+ pressing the stack expands it into a readable column. Pressing outside collapses
152
+ it again, and timers pause for as long as it is expanded.
153
+
154
+ Measured on a 1280px viewport with three rows, front to back: `scaleX` 1 / 0.959 /
155
+ 0.918 at 0 / -8 / -16px, expanding to 1 / 1 / 1 at 0 / -54 / -108px.
156
+
157
+ This is a deliberate deviation from sonner-**native**, which defaults it off —
158
+ that default is right for a phone and wrong for the desktop consumers Bloom also
159
+ serves. Two consequences to know about:
160
+
161
+ - Only the front row's action button is reachable while the stack is collapsed.
162
+ Expand it (a press) to reach the others.
163
+ - While more than one toast is live, a press on the row toggles the stack **and**
164
+ still calls the toast's own `onPress`.
165
+ - No press on a stacked row is inert: it dismisses if it lands on a ✕ that is
166
+ actually rendered (`closeButton` and `dismissible` both on), and otherwise
167
+ expands or collapses like the rest of the row.
168
+
169
+ Opt out per outlet for a surface that must show several toasts at once with no
170
+ interaction:
171
+
172
+ ```tsx
173
+ <ToastOutlet enableStacking={false} />
174
+ ```
175
+
176
+ ### Hover (web)
177
+
178
+ On web a **mouse** over the stack expands it and moving away collapses it again,
179
+ the way sonner does — no prop, no setup. Press keeps working everywhere and is the
180
+ only trigger on native and on touch: the handlers ignore any pointer that is not a
181
+ mouse, so a tap cannot expand-then-collapse the stack from its own
182
+ `pointerenter`/`pointerleave` pair. While a mouse is over the stack, a press does
183
+ not toggle the expansion (hover owns it there) but still runs the toast's `onPress`.
184
+
185
+ **The pointer pauses the auto-close timers for as long as it is over a toast**, so
186
+ hovering to read one cannot let it expire under the cursor. Measured: three rows
187
+ with the default 3000ms duration survive 5s of hover, then resume when the pointer
188
+ leaves. This holds with `enableStacking={false}` too — there the pointer pauses
189
+ without expanding anything, since a flat column has nothing to open.
190
+
191
+ Dismissing a row **out of** a hovered stack leaves the rest expanded and still
192
+ paused, rather than collapsing under the cursor and resuming. Without a pointer
193
+ holding it — on native, or once the mouse leaves — a stack still collapses itself
194
+ once one row is left, which is invisible (a lone row sits at the same offset and
195
+ scale either way) and is what stops that last toast's paused timer hanging forever.
196
+
197
+ ## Options
198
+
199
+ Per-toast options (`ToastOptions`):
200
+
201
+ | Option | Type | Description |
202
+ |--------|------|-------------|
203
+ | `id?` | `string \| number` | Stable id. Reusing one updates that toast in place. |
204
+ | `type?` | `'default' \| 'success' \| 'error' \| 'warning' \| 'info' \| 'loading'` | Visual variant. `'default'` is neutral. |
205
+ | `duration?` | `number` | Milliseconds before auto-dismiss. Defaults to `3000`. `Infinity` never auto-closes. |
206
+ | `description?` | `string` | Secondary text under the title. |
207
+ | `action?` | `{ label, onClick }` \| `ReactNode` | Primary button, or your own node. |
208
+ | `cancel?` | `{ label, onClick }` \| `ReactNode` | Secondary button; the object form also dismisses. |
209
+ | `closeButton?` | `boolean` | Show the ✕ affordance. |
210
+ | `dismissible?` | `boolean` | Allow swipe / close. Defaults to `true`. |
211
+ | `position?` | `'top-center' \| 'bottom-center' \| 'center'` | Override the outlet's position for this toast. |
212
+ | `icon?` | `ReactNode` | Replaces the variant icon. |
213
+ | `richColors?` | `boolean` | Show the status colour more prominently: it moves from the icon alone to the border and title as well. The surface stays neutral, as do the description, close button and action colours. No effect on `loading` or a variant-less toast, which have no status colour. |
214
+ | `important?` | `boolean` | Announce assertively to screen readers. |
215
+ | `onDismiss?` / `onAutoClose?` | `(id) => void` | Called when the toast goes away. |
216
+ | `onPress?` | `() => void` | Press anywhere on the row. |
217
+ | `style?` / `styles?` / `unstyled?` | — | Escape hatches for the row's own styling. |
218
+ | `animation?` | `{ enter?, exit? }` | A predefined Reanimated builder, a `Keyframe`, or `'default'`. On web, prefer a **predefined** builder for `enter`: a custom `Keyframe` handed to Reanimated's `entering` gets frozen in place by its web layout-animation cleanup, which breaks the stack. Bloom's own default enter avoids that path entirely. |
219
+
220
+ Outlet options (`ToasterProps`) cover `position`, `offset`, `gap`, `visibleToasts`,
221
+ `duration`, `swipeToDismissDirection`, `closeButton`, `richColors`,
222
+ `enableStacking`, `autoWiggleOnUpdate`, `pauseWhenPageIsHidden`, `icons`,
223
+ `toastOptions` and `positionerStyle`. Their defaults all come from the exported
224
+ `toastDefaults`, so `<ToastOutlet position="top-center" />` changes only that one.
225
+
226
+ <Callout variant="info" title="Documented no-ops">
227
+ `theme` and `invert` are accepted but do nothing: light/dark and per-subtree
228
+ recolouring belong to `BloomThemeProvider` / `BloomColorScope`.
229
+ </Callout>
230
+
231
+ ## Patterns
232
+
233
+ ### Promise toast
234
+
235
+ The pending row shows a spinner; the promise's result is formatted into the final
236
+ title. `success` is a formatter, `error` is a formatter **or** a plain string:
237
+
238
+ ```tsx
239
+ toast.promise(saveProfile(), {
240
+ loading: 'Saving…',
241
+ success: (profile) => `Saved ${profile.name}`,
242
+ error: (err) => `Failed: ${err instanceof Error ? err.message : 'unknown'}`,
243
+ });
244
+ ```
245
+
246
+ A promise toast cannot be swiped away while it is pending, and its auto-close timer
247
+ only starts once it settles.
248
+
249
+ ### Action toast
250
+
251
+ ```tsx
252
+ toast('Item moved to trash', {
253
+ action: {
254
+ label: 'Undo',
255
+ onClick: () => restore(item.id),
256
+ },
257
+ });
258
+ ```
259
+
260
+ ### Fully custom row
261
+
262
+ `toast.custom` hands the whole row to your element — Bloom renders no surface,
263
+ icon or buttons around it, but still animates, stacks and swipes it:
264
+
265
+ ```tsx
266
+ toast.custom(<MyRow />, { duration: 8000 });
267
+ ```
268
+
269
+ ### Above a native iOS modal
270
+
271
+ Bloom does not depend on `react-native-screens`. Inject the overlay host yourself
272
+ when a toast has to appear above a native modal:
273
+
274
+ ```tsx
275
+ import { FullWindowOverlay } from 'react-native-screens';
276
+
277
+ <ToastOutlet ToasterOverlayWrapper={FullWindowOverlay} />
278
+ ```
279
+
280
+ <Callout variant="info" title="Toast vs alert()">
281
+ Reach for `toast()` for passive feedback (the user doesn't have to react). Use [`alert()`](/docs/bloom/main/alert) when you need a one-shot confirmation that blocks the rest of the UI.
282
+ </Callout>
283
+
284
+ Upgrading from 0.50.x? `show`, `api`, `DURATION`, `type Toast` and the composition
285
+ exports were removed, and the default position moved to `bottom-center`.
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Tooltip
3
+ description: A hint about an already-labelled control — controlled, because what opens it differs by platform.
4
+ order: 39
5
+ ---
6
+
7
+ # Tooltip
8
+
9
+ A short hint attached to a control.
10
+
11
+ ```tsx
12
+ import { Tooltip, TooltipTrigger, TooltipTextBubble } from '@oxyhq/bloom/tooltip';
13
+
14
+ const [visible, setVisible] = useState(false);
15
+
16
+ <Tooltip visible={visible} onVisibleChange={setVisible} position="top">
17
+ <TooltipTrigger>
18
+ <Pressable
19
+ onHoverIn={() => setVisible(true)}
20
+ onHoverOut={() => setVisible(false)}
21
+ onPress={copy}
22
+ accessibilityLabel="Copy link"
23
+ >
24
+ <LinkIcon />
25
+ </Pressable>
26
+ </TooltipTrigger>
27
+ <TooltipTextBubble>Copies the link to your clipboard</TooltipTextBubble>
28
+ </Tooltip>
29
+ ```
30
+
31
+ ## Why it is controlled
32
+
33
+ What should open a tooltip differs by platform and by trigger — hover for a
34
+ desktop pointer, long-press on touch, focus for a keyboard — and the right
35
+ answer depends on what the trigger already does with a press. `Tooltip` takes
36
+ `visible` and leaves that decision at the call site rather than guessing.
37
+
38
+ `TooltipTextBubble` renders its own content surface, so it is a **sibling** of
39
+ the trigger inside `Tooltip`, not something you wrap in a `TooltipContent`.
40
+
41
+ ## A tooltip is never the only place the information appears
42
+
43
+ It cannot be reached by touch without an interaction that also does something
44
+ else, and it dismisses on the next tap. So:
45
+
46
+ - The control still needs its own `accessibilityLabel`. A tooltip is a hint
47
+ about a labelled control, not a substitute for the label.
48
+ - Nothing required to complete a task goes in one. If the user must know it,
49
+ put it on the page.
50
+
51
+ ## `position` is a preference
52
+
53
+ A bubble near a screen edge is nudged back into view, so `position="top"` on an
54
+ element at the top of the viewport still renders somewhere readable rather than
55
+ off screen.
56
+
57
+ ## Two platform notes
58
+
59
+ The **web** tooltip is not portaled and takes no rank in the overlay stack; the
60
+ **native** one is portaled and does. So on native it participates in overlay
61
+ ordering and on web it does not — check for a `Portal` before reasoning about
62
+ what paints above what.
63
+
64
+ **Known gap on native:** `TooltipTrigger` measures in page coordinates while
65
+ the content renders into the root portal group, so a tooltip inside a
66
+ `BottomSheet` portals *outside* that window and lands in the wrong place.
67
+ Fixing it needs a real device — jest cannot see a native window boundary.