@oxyhq/bloom 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +16 -4
  2. package/docs/accordion.mdx +69 -0
  3. package/docs/activity-heatmap.mdx +71 -0
  4. package/docs/admonition.mdx +77 -0
  5. package/docs/alert-dialog.mdx +75 -0
  6. package/docs/alert.mdx +93 -0
  7. package/docs/animated-check.mdx +53 -0
  8. package/docs/aspect-ratio.mdx +45 -0
  9. package/docs/avatar-group.mdx +91 -0
  10. package/docs/avatar.mdx +126 -0
  11. package/docs/badge.mdx +74 -0
  12. package/docs/benefit-list.mdx +56 -0
  13. package/docs/bottom-sheet.mdx +103 -0
  14. package/docs/button.mdx +218 -0
  15. package/docs/card.mdx +106 -0
  16. package/docs/checkbox.mdx +59 -0
  17. package/docs/chip.mdx +53 -0
  18. package/docs/code.mdx +50 -0
  19. package/docs/combobox.mdx +64 -0
  20. package/docs/command.mdx +85 -0
  21. package/docs/composition-bar.mdx +63 -0
  22. package/docs/connection-dots.mdx +50 -0
  23. package/docs/connection-status.mdx +56 -0
  24. package/docs/content-panel.mdx +62 -0
  25. package/docs/context-menu.mdx +65 -0
  26. package/docs/design-tokens.mdx +254 -0
  27. package/docs/dialog.mdx +277 -0
  28. package/docs/divider.mdx +56 -0
  29. package/docs/dot-grid-meter.mdx +66 -0
  30. package/docs/dropdown-menu.mdx +161 -0
  31. package/docs/error-boundary.mdx +72 -0
  32. package/docs/fab.mdx +67 -0
  33. package/docs/field.mdx +70 -0
  34. package/docs/fill.mdx +58 -0
  35. package/docs/fonts.mdx +144 -0
  36. package/docs/frosted-icon-button.mdx +60 -0
  37. package/docs/getting-started.mdx +75 -0
  38. package/docs/grid.mdx +64 -0
  39. package/docs/hooks.mdx +81 -0
  40. package/docs/icon-circle.mdx +66 -0
  41. package/docs/icons.mdx +69 -0
  42. package/docs/image-aspect-ratio-cache.mdx +63 -0
  43. package/docs/image-resolver.mdx +66 -0
  44. package/docs/index.mdx +44 -0
  45. package/docs/input-group.mdx +58 -0
  46. package/docs/item.mdx +69 -0
  47. package/docs/kbd.mdx +57 -0
  48. package/docs/label.mdx +50 -0
  49. package/docs/link-preview.mdx +66 -0
  50. package/docs/list.mdx +72 -0
  51. package/docs/loading.mdx +129 -0
  52. package/docs/media-inset-border.mdx +52 -0
  53. package/docs/menubar.mdx +86 -0
  54. package/docs/migrating-to-1.0.mdx +434 -0
  55. package/docs/motion.mdx +83 -0
  56. package/docs/overlay.mdx +75 -0
  57. package/docs/popover.mdx +73 -0
  58. package/docs/portal.mdx +63 -0
  59. package/docs/pressable-scale.mdx +51 -0
  60. package/docs/profile-card.mdx +66 -0
  61. package/docs/progressive-blur.mdx +54 -0
  62. package/docs/prompt-input.mdx +183 -0
  63. package/docs/provider.mdx +64 -0
  64. package/docs/radio-indicator.mdx +60 -0
  65. package/docs/radio.mdx +65 -0
  66. package/docs/scroll.mdx +66 -0
  67. package/docs/search.mdx +67 -0
  68. package/docs/segmented-control.mdx +76 -0
  69. package/docs/select.mdx +93 -0
  70. package/docs/settings-list.mdx +186 -0
  71. package/docs/skeleton.mdx +66 -0
  72. package/docs/slider.mdx +73 -0
  73. package/docs/stat-bar.mdx +59 -0
  74. package/docs/stat-widgets.mdx +33 -0
  75. package/docs/styles.mdx +61 -0
  76. package/docs/subtle-hover.mdx +53 -0
  77. package/docs/surfaces.mdx +72 -0
  78. package/docs/switch.mdx +90 -0
  79. package/docs/tab-bar.mdx +386 -0
  80. package/docs/tabs.mdx +75 -0
  81. package/docs/text-field.mdx +187 -0
  82. package/docs/theme.mdx +67 -0
  83. package/docs/toast.mdx +285 -0
  84. package/docs/tooltip.mdx +67 -0
  85. package/docs/typography.mdx +88 -0
  86. package/docs/user-hover-card.mdx +140 -0
  87. package/docs/zoomable-image-gallery.mdx +66 -0
  88. package/lib/commonjs/design-tokens/color-roles.js +21 -1
  89. package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
  90. package/lib/commonjs/design-tokens/index.js +6 -0
  91. package/lib/commonjs/design-tokens/index.js.map +1 -1
  92. package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
  93. package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
  94. package/lib/commonjs/design-tokens/theme-css.js +1 -0
  95. package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
  96. package/lib/commonjs/design-tokens/theme.css +3 -0
  97. package/lib/commonjs/design-tokens/tokens-json.js +13 -4
  98. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
  99. package/lib/commonjs/design-tokens/tokens.json +6227 -265
  100. package/lib/commonjs/index.js +7 -0
  101. package/lib/commonjs/index.js.map +1 -1
  102. package/lib/commonjs/index.web.js +7 -0
  103. package/lib/commonjs/index.web.js.map +1 -1
  104. package/lib/commonjs/theme/ambient-store.js +16 -1
  105. package/lib/commonjs/theme/ambient-store.js.map +1 -1
  106. package/lib/commonjs/theme/color-policy.js +30 -33
  107. package/lib/commonjs/theme/color-policy.js.map +1 -1
  108. package/lib/commonjs/theme/color-presets.js +413 -166
  109. package/lib/commonjs/theme/color-presets.js.map +1 -1
  110. package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
  111. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  112. package/lib/commonjs/theme/glass-colors.js +18 -21
  113. package/lib/commonjs/theme/glass-colors.js.map +1 -1
  114. package/lib/commonjs/theme/index.js +30 -0
  115. package/lib/commonjs/theme/index.js.map +1 -1
  116. package/lib/commonjs/theme/index.web.js +30 -0
  117. package/lib/commonjs/theme/index.web.js.map +1 -1
  118. package/lib/module/design-tokens/color-roles.js +20 -0
  119. package/lib/module/design-tokens/color-roles.js.map +1 -1
  120. package/lib/module/design-tokens/index.js +2 -2
  121. package/lib/module/design-tokens/index.js.map +1 -1
  122. package/lib/module/design-tokens/tailwind-preset.js +4 -2
  123. package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
  124. package/lib/module/design-tokens/theme-css.js +2 -1
  125. package/lib/module/design-tokens/theme-css.js.map +1 -1
  126. package/lib/module/design-tokens/theme.css +3 -0
  127. package/lib/module/design-tokens/tokens-json.js +14 -5
  128. package/lib/module/design-tokens/tokens-json.js.map +1 -1
  129. package/lib/module/design-tokens/tokens.json +6227 -265
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/index.web.js +1 -1
  133. package/lib/module/index.web.js.map +1 -1
  134. package/lib/module/theme/ambient-store.js +16 -1
  135. package/lib/module/theme/ambient-store.js.map +1 -1
  136. package/lib/module/theme/color-policy.js +30 -33
  137. package/lib/module/theme/color-policy.js.map +1 -1
  138. package/lib/module/theme/color-presets.js +412 -165
  139. package/lib/module/theme/color-presets.js.map +1 -1
  140. package/lib/module/theme/color-scope/seed-scope.js +2 -1
  141. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  142. package/lib/module/theme/glass-colors.js +18 -21
  143. package/lib/module/theme/glass-colors.js.map +1 -1
  144. package/lib/module/theme/index.js +1 -1
  145. package/lib/module/theme/index.js.map +1 -1
  146. package/lib/module/theme/index.web.js +1 -1
  147. package/lib/module/theme/index.web.js.map +1 -1
  148. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
  149. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
  151. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
  155. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/index.d.ts +2 -2
  157. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  159. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
  163. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  165. package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
  166. package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  168. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
  170. package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
  171. package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
  172. package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
  173. package/lib/typescript/module/design-tokens/index.d.ts +3 -3
  174. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  175. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
  176. package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
  177. package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
  178. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
  179. package/lib/typescript/module/index.d.ts +2 -2
  180. package/lib/typescript/module/index.d.ts.map +1 -1
  181. package/lib/typescript/module/index.web.d.ts +2 -2
  182. package/lib/typescript/module/index.web.d.ts.map +1 -1
  183. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
  184. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  185. package/lib/typescript/module/theme/color-presets.d.ts +386 -62
  186. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  187. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  188. package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
  189. package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
  190. package/lib/typescript/module/theme/index.d.ts +2 -2
  191. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  192. package/lib/typescript/module/theme/index.web.d.ts +2 -2
  193. package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
  194. package/package.json +5 -2
  195. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  196. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  197. package/src/__tests__/support/press-host.ts +30 -0
  198. package/src/__tests__/support/rendered-style.ts +99 -0
  199. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  200. package/src/design-tokens/color-roles.ts +21 -0
  201. package/src/design-tokens/index.ts +3 -2
  202. package/src/design-tokens/tailwind-preset.ts +3 -2
  203. package/src/design-tokens/theme-css.ts +2 -1
  204. package/src/design-tokens/theme.css +3 -0
  205. package/src/design-tokens/tokens-json.ts +29 -9
  206. package/src/design-tokens/tokens.json +6227 -265
  207. package/src/glass/GlassSurface.tsx +2 -2
  208. package/src/index.ts +2 -0
  209. package/src/index.web.ts +2 -0
  210. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
  211. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  212. package/src/theme/ambient-store.ts +21 -1
  213. package/src/theme/color-policy.ts +33 -38
  214. package/src/theme/color-presets.ts +473 -146
  215. package/src/theme/color-scope/seed-scope.ts +2 -1
  216. package/src/theme/glass-colors.ts +18 -21
  217. package/src/theme/index.ts +16 -1
  218. package/src/theme/index.web.ts +16 -1
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: ContextMenu
3
+ description: The menu a right-click opens on web and a long press opens on native.
4
+ order: 16
5
+ ---
6
+
7
+ # ContextMenu
8
+
9
+ The secondary menu for an object on screen. Right-click on web, long press on native.
10
+
11
+ Its parts are shadcn's, and its rows are the SAME rows `DropdownMenu` and `Menubar` publish — one implementation under three prefixes, so a row behaves identically wherever it is.
12
+
13
+ ## Basic
14
+
15
+ ```tsx
16
+ import {
17
+ ContextMenu,
18
+ ContextMenuContent,
19
+ ContextMenuItem,
20
+ ContextMenuSeparator,
21
+ ContextMenuShortcut,
22
+ ContextMenuTrigger,
23
+ } from '@oxyhq/bloom/context-menu';
24
+
25
+ <ContextMenu>
26
+ <ContextMenuTrigger label="Photo actions">
27
+ <Image source={photo.source} style={styles.thumb} />
28
+ </ContextMenuTrigger>
29
+
30
+ <ContextMenuContent>
31
+ <ContextMenuItem
32
+ onPress={() => share(photo)}
33
+ trailing={<ContextMenuShortcut>⌘S</ContextMenuShortcut>}>
34
+ Share
35
+ </ContextMenuItem>
36
+ <ContextMenuItem onPress={() => save(photo)}>Save to library</ContextMenuItem>
37
+ <ContextMenuSeparator />
38
+ <ContextMenuItem variant="destructive" onPress={() => remove(photo)}>
39
+ Delete
40
+ </ContextMenuItem>
41
+ </ContextMenuContent>
42
+ </ContextMenu>
43
+ ```
44
+
45
+ `asChild` composes the trigger onto your own element instead of wrapping it, exactly as it does on `DropdownMenu`.
46
+
47
+ ## It opens where the pointer is
48
+
49
+ On web the menu anchors to the CLICK POINT, not to the trigger's box: right-clicking the bottom-right corner of a large card opens the menu there, not under the card's top-left. A click point is a zero-area anchor, which the same fit / flip / clamp arithmetic every other Bloom surface uses already handles.
50
+
51
+ On native there is no cursor to anchor to, so a long press presents the same bottom sheet `DropdownMenu` uses.
52
+
53
+ ## No `open` prop, on purpose
54
+
55
+ `ContextMenu` is uncontrolled by construction. Its open POSITION comes from the pointer event that opened it, so there is no `open` a caller could set that would mean anything — Radix's `ContextMenu.Root` takes the same view. What you get instead is `onOpenChange`:
56
+
57
+ ```tsx
58
+ <ContextMenu onOpenChange={(open) => setDimmed(open)}>
59
+ ```
60
+
61
+ ## Rows
62
+
63
+ Everything `DropdownMenu` documents applies, under the `ContextMenu` prefix: `ContextMenuItem`, `ContextMenuCheckboxItem`, `ContextMenuRadioGroup` / `ContextMenuRadioItem`, `ContextMenuLabel`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuGroup`, and `ContextMenuSub` / `ContextMenuSubTrigger` / `ContextMenuSubContent`.
64
+
65
+ 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) for the hover, keyboard and placement behaviour, which is shared.
@@ -0,0 +1,254 @@
1
+ ---
2
+ title: Design Tokens
3
+ description: The centralized semantic Tailwind / NativeWind vocabulary shared by every Oxy app.
4
+ ---
5
+
6
+ # Design Tokens
7
+
8
+ `@oxyhq/bloom/design-tokens` ships the **Oxy Unified Design Language**: a
9
+ namespaced, semantic set of utility classes (`bg-fill`, `text-text-tertiary`,
10
+ `p-space-8`, `rounded-radius-20`, `text-body`, `font-body`, `shadow-s`) that
11
+ resolve to the SAME class names on **web (Tailwind)** and **native (NativeWind)**.
12
+
13
+ Color roles are aliases onto Bloom's existing resolved theme tokens
14
+ (`--background`, `--surface`, `--primary`, …), so they follow the active
15
+ light/dark mode and color preset automatically. The vocabulary is **additive** —
16
+ existing utilities (`bg-background`, `text-muted-foreground`, `rounded-lg`) keep
17
+ working.
18
+
19
+ ## Preset recipes and runtime colour
20
+
21
+ Bloom ships **34 named recipes**, authored once in the type-checked
22
+ `COLOR_PRESET_REGISTRY`. A recipe is not a frozen light/dark token table: it is
23
+ an identity seed, a scheme variant and optionally explicit secondary/tertiary
24
+ seeds. `BloomThemeProvider`, `getResolvedTokens()` and `BloomColorScope` run
25
+ those seeds through the same colour policy for the active mode and scope.
26
+ Arbitrary consumer seeds remain supported through `buildThemeFromSeed()` and
27
+ `BloomSeedScope`; the named catalog is a set of useful starting points, not the
28
+ limit of the dynamic engine.
29
+
30
+ ```ts
31
+ import {
32
+ COLOR_PRESET_REGISTRY,
33
+ COLOR_PRESET_FAMILY_REGISTRY,
34
+ COLOR_PRESET_GROUPS,
35
+ } from '@oxyhq/bloom';
36
+
37
+ const cobalt = COLOR_PRESET_REGISTRY.find((recipe) => recipe.name === 'cobalt');
38
+ cobalt?.displayName; // 'Cobalt + Signal Yellow'
39
+ cobalt?.family; // 'ocean'
40
+ cobalt?.tertiaryHex; // '#ffd000'
41
+
42
+ COLOR_PRESET_FAMILY_REGISTRY[0].displayName; // stable family copy and order
43
+ COLOR_PRESET_GROUPS.ocean.presets; // ordered recipes for a picker
44
+ ```
45
+
46
+ The 16 `pairing: 'curated'` recipes pin a complementary `tertiaryHex`. The
47
+ identity remains `--primary`; the standout action is `--tertiary`, which is the
48
+ role used by FABs and compose actions. The other 18 recipes derive their accent
49
+ families dynamically. Every recipe exposes `displayName`, `description`,
50
+ `family`, `pairing` and `featured` metadata, so consumer pickers do not need a
51
+ second name/category map. Historical `APP_COLOR_PRESETS`, name arrays, hex
52
+ lookup and access-gate arrays are derived from this registry and retain their
53
+ existing public shapes.
54
+
55
+ For visual review, run Storybook and open **Theme / Color System Playground**
56
+ (`src/theme/ColorSystemLab.stories.tsx`). It keeps signed-in and public Mention
57
+ views, renders both modes from `getResolvedTokens()`, and filters the complete
58
+ registry by family or curated/derived pairing.
59
+
60
+ ## Opt in
61
+
62
+ ### Native (NativeWind) and Tailwind v3 web
63
+
64
+ Add the Bloom preset to your `tailwind.config.js`:
65
+
66
+ ```js
67
+ const { bloomTailwindPreset } = require('@oxyhq/bloom/tailwind-preset');
68
+
69
+ module.exports = {
70
+ presets: [bloomTailwindPreset],
71
+ content: [/* your existing globs */],
72
+ theme: { extend: {/* your existing app overrides */} },
73
+ };
74
+ ```
75
+
76
+ That single line adds every semantic utility. Your `BloomThemeProvider` already
77
+ populates the underlying `--x` color tokens at runtime.
78
+
79
+ ### Tailwind v4 web (auth, console, website) and CSS-first NativeWind (Homiio)
80
+
81
+ Tailwind v4 apps configure tokens in CSS. **Import Bloom's shipped theme CSS —
82
+ never paste the tokens into your own `global.css`.** Bloom ships a static
83
+ `theme.css` (generated from the same source as `bloomThemeCss()`, so JS and CSS
84
+ cannot drift) at a dedicated export:
85
+
86
+ ```css
87
+ /* your global.css — after the Tailwind import */
88
+ @import "tailwindcss";
89
+ @import "@oxyhq/bloom/design-tokens/theme.css";
90
+ ```
91
+
92
+ That single `@import` registers the full Bloom `@theme` vocabulary (color-role
93
+ aliases, spacing, radius, border-width, typography, shadow), so utilities like
94
+ `rounded-radius-28`, `p-space-8`, and `text-body` resolve without hand-copying
95
+ any `--radius-radius-*` / `--spacing-*` block. The aliases are
96
+ `--color-<role>: var(--canonical)` — direct `var(--x)` references (never
97
+ `hsl(var(--x))`, per Bloom's web CSS-var contract).
98
+
99
+ Keep only your **app-local** color seeds / `:root` overrides in `global.css`.
100
+ Do NOT re-declare Bloom's radius or spacing scales locally — the imported CSS is
101
+ the single authority.
102
+
103
+ If you need the block inline (e.g. an app that assembles its stylesheet at build
104
+ time) rather than via `@import`, `bloomThemeBlock()` / `bloomThemeCss()` return
105
+ the exact same content programmatically:
106
+
107
+ ```ts
108
+ import { bloomThemeBlock } from '@oxyhq/bloom/design-tokens';
109
+ // write `bloomThemeBlock()` into your generated global.css
110
+ ```
111
+
112
+ ### The pre-JS palette (prerendered apps)
113
+
114
+ `theme.css` is the **alias** layer: `--color-background: var(--background)`. It
115
+ says nothing about what `--background` *is* — `BloomThemeProvider` writes that at
116
+ startup. So between first paint and hydration, a prerendered page shows whatever
117
+ palette its own stylesheet declares, and a hand-written one is a second source of
118
+ truth that drifts from the preset it claims to mirror.
119
+
120
+ Generate it instead, at build time:
121
+
122
+ ```ts
123
+ import { getPresetVars, buildSeedScopeVars } from '@oxyhq/bloom/design-tokens';
124
+
125
+ // Document root: canonical tokens are all it needs.
126
+ getPresetVars('oxy', 'dark'); // { '--background': 'rgb(16 13 16)', … }
127
+
128
+ // A scoped block — a brand section, a forced-dark region — needs the
129
+ // `--color-x` aliases too, because an alias substitutes where it is DECLARED:
130
+ // overriding `--background` on a subtree does not move a `--color-background`
131
+ // declared at `:root`. That is why hand-written brand themes write every colour
132
+ // twice. `buildSeedScopeVars` returns both namespaces from one seed.
133
+ buildSeedScopeVars({ seed: '#7c5aed', mode: 'dark' });
134
+ ```
135
+
136
+ `buildSeedScopeVars` also writes the ROLE vocabulary (`withScopeAliases`)
137
+ alongside the colour aliases — omitting it is what makes a scoped block paint
138
+ near-black text over a dark photo in forced-dark mode, since the role that
139
+ resolves foreground-over-fill never got scoped with everything else.
140
+
141
+ A preset's own `hex` + `variant` reproduces `getPresetVars` token-for-token, so
142
+ the document root and a brand scope go through the same code path. Both
143
+ functions are pure — no react, no react-native — so a plain node/bun script can
144
+ import them.
145
+
146
+ ### Consumers that are not browsers
147
+
148
+ A consumer that cannot run a stylesheet — native UI code, a design tool, a
149
+ codegen step — reads the resolved tokens as data instead. Bloom ships them in
150
+ W3C Design Tokens (DTCG) format, generated from the same colour engine, so the
151
+ JSON and the CSS cannot disagree:
152
+
153
+ ```ts
154
+ import tokens from '@oxyhq/bloom/design-tokens/tokens.json';
155
+
156
+ tokens.color.oxy.dark.card.$value; // '#3b323c' — the value of --card
157
+ tokens.color.teal.light.primary.$value; // '#1d6a75'
158
+ tokens.spacing['space-8'].$value; // '8px'
159
+ tokens.typography.body.fontWeight.$value; // 400
160
+ ```
161
+
162
+ The JSON is an interoperability artifact generated by `generate:tokens-json`,
163
+ not an editable source. Each preset group also carries a `so.oxy.bloom`
164
+ extension with its seed recipe and picker metadata (family, description,
165
+ pairing, featured and optional secondary/tertiary seeds). Keeping the authored
166
+ registry in TypeScript gives duplicate/shape checks and inferred identifier
167
+ types; generating JSON from it gives non-TypeScript tools the same data without
168
+ introducing a second catalog.
169
+
170
+ `color.<preset>.<scheme>.<token>` covers every preset Bloom ships and both
171
+ schemes. Token names are the CSS custom properties with the leading `--`
172
+ removed, so the mapping to `getPresetVars` output is mechanical. Values are
173
+ sRGB hex — `#rrggbb`, or `#rrggbbaa` for the translucent `-subtle` family.
174
+ Font families and shadows are deliberately absent: families resolve to
175
+ `var(--bloom-font-*)`, which is meaningless off the web, and shadows ship as a
176
+ platform-forked style object (`bloomShadowStyle`). If you need the same data in
177
+ memory rather than from the file, `bloomDesignTokens()` from
178
+ `@oxyhq/bloom/design-tokens` returns it.
179
+
180
+ ## Color roles
181
+
182
+ | Utility | Role | Canonical token |
183
+ | --- | --- | --- |
184
+ | `bg-bg` | page background | `--background` |
185
+ | `bg-fill` | default surface | `--card` |
186
+ | `bg-fill-secondary` | muted fill | `--muted` |
187
+ | `bg-fill-hover` | secondary hover | `--accent` |
188
+ | `bg-fill-brand` | brand fill | `--primary` |
189
+ | `bg-fill-brand-hover` | brand hover | `--ring` |
190
+ | `bg-fill-inverse` | inverse fill | `--foreground` |
191
+ | `bg-fill-inverse-hover` | inverse hover | `--muted-foreground` |
192
+ | `bg-fill-placeholder` | disabled surface | `--muted` |
193
+ | `text-text` | primary text | `--foreground` |
194
+ | `text-text-secondary` | secondary text | `--muted-foreground` |
195
+ | `text-text-tertiary` | tertiary text | `--muted-foreground` |
196
+ | `text-text-inverse` | text on brand | `--primary-foreground` |
197
+ | `text-text-fixed-light` | always-light text | `--primary-foreground` |
198
+ | `text-text-placeholder` | placeholder text | `--muted-foreground` |
199
+ | `border-border` | default border | `--border` |
200
+ | `border-border-image` | hairline color | `--border` |
201
+ | `border-border-secondary` | softer border | `--input` |
202
+ | `border-border-input` | input border | `--input` |
203
+ | `border-border-input-active` | focused input | `--ring` |
204
+
205
+ ## The neutral surface ramp
206
+
207
+ `--background`, `--surface`, `--popover`, `--muted`/`--accent` and `--card` are a
208
+ stack: several components carry meaning **only** in the step between two of them —
209
+ an `ActivityHeatmap` empty cell, `Card variant="filled"`, a `Code` block, the
210
+ `StatBar` and `CompositionBar` tracks, `DotGridMeter`'s off dots, a filled `Tabs`
211
+ track, a selected `Item`, the solid `TabBar`, a plain `Card` on a panel, the
212
+ `Toast` action, and the `Avatar`/`BenefitList`/`LinkPreviewCard` placeholders.
213
+
214
+ Those tones are owned by `SURFACE_RAMP` in `theme/color-policy.ts` rather than
215
+ taken from Material 3's container roles. M3's ramp steps by 2 tones, which at
216
+ these lightnesses is about **1.2 ΔE00** — below the just-noticeable difference for
217
+ two large flat patches. Measured across all 34 presets, **4 tones (~2.4 ΔE00)** is
218
+ the first spacing at which every pair separates.
219
+
220
+ `--background` is deliberately not part of the ramp: the page tone is every
221
+ consuming app's page colour, so the rungs below it carry the spacing instead.
222
+ `--card` is not either — it sits on the far side of the page in light (tone 100,
223
+ white) and at the top of the ramp in dark, so it is expressed as the engine role
224
+ that means exactly that in each mode.
225
+
226
+ If you move any of these, measure **every** pair rather than the one you are
227
+ fixing: the light tones 92/94/96/98/100 are exactly `--muted`, `--popover`,
228
+ `--surface`, `--content-area` and `--card`, so any new value for one lands on
229
+ another unless you check.
230
+
231
+ ## Scales
232
+
233
+ - **Spacing** (px): `space-2/4/8/12/16/20/24/32` + `screen-margin` (20).
234
+ `p-space-8`, `gap-space-20`, `px-screen-margin`.
235
+ - **Radius** (px): `radius-8/12/20/28/max`. `rounded-radius-20`.
236
+ - **Hairline border**: `border-hairline` (0.5px), pair with `border-border-image`.
237
+ - **Typography**: `text-<role>` (size/line-height) + `font-<role>`
238
+ (family/weight) for `caption, bodySmall, bodyTitleSmall, body, subtitle,
239
+ sectionTitle, headerBold, buttonLarge`.
240
+ - **Shadow**: `shadow-s`, `shadow-m` (web box-shadow). On native surfaces apply
241
+ `bloomShadowStyle('s' | 'm')` (returns RN elevation/shadow props).
242
+
243
+ ### Type scale
244
+
245
+ | Role | Size / line | Weight | Family |
246
+ | --- | --- | --- | --- |
247
+ | `caption` | 11 / 14 | 400 | sans |
248
+ | `bodySmall` | 13 / 18 | 400 | sans |
249
+ | `bodyTitleSmall` | 13 / 18 | 600 | sans |
250
+ | `body` | 15 / 22 | 400 | sans |
251
+ | `subtitle` | 17 / 24 | 500 | sans |
252
+ | `sectionTitle` | 20 / 26 | 600 | sans |
253
+ | `headerBold` | 28 / 34 | 700 | display |
254
+ | `buttonLarge` | 17 / 22 | 600 | sans |
@@ -0,0 +1,277 @@
1
+ ---
2
+ title: Dialog
3
+ description: A single <Dialog> for every overlay surface — centered modal, side-sheet, or bottom-sheet.
4
+ order: 3
5
+ ---
6
+
7
+ # Dialog
8
+
9
+ A single `<Dialog>` component for every overlay surface — centered modal, side-sheet (left/right drawer), or bottom-sheet — controlled by the `placement` prop. Same component, same props on every platform.
10
+
11
+ <Callout variant="warning" title="Required providers (native)">
12
+ Your app root **must** be wrapped with `GestureHandlerRootView` from `react-native-gesture-handler` for the bottom-sheet pan gestures to work. See [Getting started](/docs/bloom/main/getting-started).
13
+ </Callout>
14
+
15
+ ## Declarative (the 90% case)
16
+
17
+ ```tsx
18
+ import { Dialog, useDialogControl, Button } from '@oxyhq/bloom';
19
+
20
+ function SignOutButton() {
21
+ const control = useDialogControl();
22
+ return (
23
+ <>
24
+ <Button onPress={() => control.open()}>Sign out</Button>
25
+
26
+ <Dialog
27
+ control={control}
28
+ title="Sign out?"
29
+ description="You'll need to enter your password to sign in again."
30
+ actions={[
31
+ { label: 'Sign out', color: 'destructive', onPress: doSignOut },
32
+ { label: 'Cancel', color: 'cancel' },
33
+ ]}
34
+ />
35
+ </>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ## Controlled open state
41
+
42
+ <Callout variant="warning" title="Prefer useDialogControl">
43
+ The controlled path fires `onClose` synchronously on `open` flipping to `false` — ahead of the exit animation. If your `onClose` handler unmounts the consumer (navigates away, removes the dialog from the tree), it races the animation still in flight. `useDialogControl()` doesn't have this footgun: it closes imperatively and only resolves once the exit finishes.
44
+ </Callout>
45
+
46
+ Pass `open` + `onClose` to drive the dialog from external state instead of `useDialogControl`:
47
+
48
+ ```tsx
49
+ <Dialog
50
+ placement="center"
51
+ open={isOpen}
52
+ onClose={() => setIsOpen(false)}
53
+ title="Confirm?"
54
+ actions={[
55
+ { label: 'OK', onPress: handleOk },
56
+ { label: 'Cancel', color: 'cancel' },
57
+ ]}
58
+ />
59
+ ```
60
+
61
+ ## Side-sheet (drawer)
62
+
63
+ ```tsx
64
+ // Fixed left side-sheet
65
+ <Dialog placement="left" control={control} title="Filters">
66
+ <FilterPanel />
67
+ </Dialog>
68
+
69
+ // Responsive: bottom-sheet on mobile, left drawer on desktop
70
+ <Dialog placement={{ base: 'bottom', md: 'left' }} control={control} title="Filters">
71
+ <FilterPanel />
72
+ </Dialog>
73
+ ```
74
+
75
+ ## Custom content
76
+
77
+ Provide any JSX as `children`. Combine with `title` to keep a consistent header. Set `contentPadding={0}` when the children own their own insets.
78
+
79
+ ```tsx
80
+ <Dialog control={control} title="Pick a tag" contentPadding={0}>
81
+ <YourCustomBody />
82
+ </Dialog>
83
+ ```
84
+
85
+ ## Pure custom
86
+
87
+ Drop the declarative props entirely — `children` owns every pixel.
88
+
89
+ ```tsx
90
+ <Dialog control={control}>
91
+ <YourEntirelyCustomLayout />
92
+ </Dialog>
93
+ ```
94
+
95
+ ## Props
96
+
97
+ | Prop | Type | Default | Description |
98
+ |------|------|---------|-------------|
99
+ | `control?` | `DialogControl` | — | From `useDialogControl()`. Omit when using controlled `open`. |
100
+ | `open?` | `boolean` | — | Controlled open state. When provided, wins over `control`. |
101
+ | `placement?` | `DialogPlacement \| ResponsiveDialogPlacement` | `'center'` | Surface anchor. See [Placement](#placement). |
102
+ | `title?` | `string` | — | Header text. |
103
+ | `description?` | `string` | — | Supporting copy below the title. |
104
+ | `actions?` | `DialogAction[]` | — | Confirmation buttons. See [DialogAction](#dialogaction). |
105
+ | `children?` | `React.ReactNode` | — | Custom content rendered after the description. |
106
+ | `onClose?` | `() => void` | — | Fires after the dialog finishes closing. In controlled mode, the host flips `open` to `false`. |
107
+ | `contentPadding?` | `number` | `20` | Inner padding of the dialog body (px). Set `0` for custom children that own their insets. |
108
+ | `width?` | `number` | `460` | Side-sheet width (px) on `left`/`right` placements. |
109
+ | `maxWidth?` | `number` | `480` | Centered-card max width (px). |
110
+ | `maxHeightRatio?` | `number` | `0.9` | Bottom-sheet max height as a fraction of the viewport height. |
111
+ | `inset?` | `{ top?; bottom?; left?; right? }` | — | Side-sheet inset (px) from the overlay container edges. |
112
+ | `showHandle?` | `boolean` | `true` | Show drag handle in bottom-sheet mode. |
113
+ | `dismissOnBackdrop?` | `boolean` | `true` | Tap backdrop to dismiss. |
114
+ | `panelStyle?` | `StyleProp<ViewStyle>` | — | Style for the panel surface. |
115
+ | `panelClassName?` | `string` | — | NativeWind class for the panel surface. |
116
+ | `containerStyle?` | `StyleProp<ViewStyle>` | — | Style for the root overlay (e.g. rail offset, theme-var scope). |
117
+ | `containerClassName?` | `string` | — | NativeWind class for the root overlay. |
118
+ | `label?` | `string` | — | Accessibility label. |
119
+ | `testID?` | `string` | — | RN testing ID. |
120
+
121
+ ## Placement
122
+
123
+ `placement` accepts a single value or a **responsive map** resolved by `useWindowDimensions()`:
124
+
125
+ ```ts
126
+ type DialogPlacement = 'center' | 'left' | 'right' | 'bottom';
127
+
128
+ type ResponsiveDialogPlacement =
129
+ | DialogPlacement
130
+ | { base: DialogPlacement; sm?: DialogPlacement; md?: DialogPlacement; lg?: DialogPlacement; xl?: DialogPlacement };
131
+ ```
132
+
133
+ Breakpoints (px): `sm` 640 / `md` 768 / `lg` 1024 / `xl` 1280. The largest breakpoint whose min-width is `<=` the viewport wins; falls back to `base` below all breakpoints.
134
+
135
+ Per-placement behavior:
136
+ - `center` — DOM-portal modal (web) / detached floating card (native).
137
+ - `left` / `right` — animated side-sheet anchored to the viewport edge.
138
+ - `bottom` — reuses the cross-platform `BottomSheet` on BOTH web and native (drag-to-dismiss + internal scroll work everywhere). Custom `children` on bottom placement are rendered in a NON-scrollable body — children own scrolling.
139
+
140
+ ### `DialogAction`
141
+
142
+ | Prop | Type | Description |
143
+ |------|------|-------------|
144
+ | `label` | `string` | Button text. |
145
+ | `color?` | `'default' \| 'cancel' \| 'destructive'` | Defaults to `'default'`. |
146
+ | `onPress?` | `(e) => void` | Invoked **after** the dialog finishes closing. |
147
+ | `disabled?` | `boolean` | |
148
+ | `shouldCloseOnPress?` | `boolean` | Defaults to `true`. Set `false` while an async action is in flight. |
149
+ | `testID?` | `string` | |
150
+
151
+ ## Blurring the backdrop on Android
152
+
153
+ A backdrop blurs its content on iOS and web with no setup. **On Android it needs
154
+ the app content wrapped in a blur target**, which `BloomProvider` composes — so
155
+ if you mount it, there is nothing to do:
156
+
157
+ ```tsx
158
+ import { BloomProvider } from '@oxyhq/bloom/provider';
159
+
160
+ <BloomProvider>{children}</BloomProvider>
161
+ ```
162
+
163
+ An app that does not mount `BloomProvider` keeps the tint-only backdrop. That is
164
+ a supported state, not a missing step — see below.
165
+
166
+ Two things are worth knowing rather than discovering:
167
+
168
+ - **It applies to the bottom-sheet path only.** `expo-blur` on Android blurs a
169
+ target view, and a blur view that is a DESCENDANT of its own target crashes
170
+ the app — `SIGSEGV`, an unbounded render-tree recursion. Only a surface in a
171
+ separate native window is safe, which means a real RN `<Modal>`: that is
172
+ `BottomSheet`, and `Dialog`'s bottom placement through it. `Dialog`'s centre
173
+ and side placements portal into the same window and keep the tint-only
174
+ backdrop. Bloom enforces this structurally — the target is simply not
175
+ reachable from a surface that has not crossed a window boundary — so there is
176
+ no wiring you can get wrong.
177
+ - **Not mounting it is a supported state**, not a broken one. The backdrop still
178
+ paints its tint at full intensity; you lose the blur and nothing else, with no
179
+ warning and no crash.
180
+
181
+ ## Web setup
182
+
183
+ Inject the CSS animations into your global styles once:
184
+
185
+ ```tsx
186
+ import { BLOOM_DIALOG_CSS } from '@oxyhq/bloom/dialog';
187
+
188
+ // In your HTML head or global CSS file:
189
+ <style>{BLOOM_DIALOG_CSS}</style>
190
+ ```
191
+
192
+ ## Examples
193
+
194
+ ### Three-action confirmation
195
+
196
+ ```tsx
197
+ <Dialog
198
+ control={control}
199
+ title="Delete project?"
200
+ description="This will permanently delete the project and all of its files."
201
+ actions={[
202
+ { label: 'Delete', color: 'destructive', onPress: handleDelete },
203
+ { label: 'Archive', onPress: handleArchive },
204
+ { label: 'Cancel', color: 'cancel' },
205
+ ]}
206
+ />
207
+ ```
208
+
209
+ ### Async confirm with loading state
210
+
211
+ ```tsx
212
+ const [busy, setBusy] = useState(false);
213
+
214
+ <Dialog
215
+ control={control}
216
+ title="Submit?"
217
+ actions={[
218
+ {
219
+ label: busy ? 'Submitting…' : 'Submit',
220
+ disabled: busy,
221
+ shouldCloseOnPress: false, // keep dialog open while we work
222
+ onPress: async () => {
223
+ setBusy(true);
224
+ try {
225
+ await submit();
226
+ control.close();
227
+ } finally {
228
+ setBusy(false);
229
+ }
230
+ },
231
+ },
232
+ { label: 'Cancel', color: 'cancel' },
233
+ ]}
234
+ />
235
+ ```
236
+
237
+ ### Form inside a dialog
238
+
239
+ ```tsx
240
+ <Dialog control={control} title="Add tag">
241
+ <TextFieldInput label="Tag name" value={name} onChangeText={setName} />
242
+ <Button onPress={() => { save(name); control.close(); }}>Save</Button>
243
+ </Dialog>
244
+ ```
245
+
246
+ ### Responsive drawer→sheet
247
+
248
+ Bottom-sheet on small screens, left drawer on `md` and wider:
249
+
250
+ ```tsx
251
+ <Dialog
252
+ placement={{ base: 'bottom', md: 'left' }}
253
+ control={control}
254
+ title="Store settings"
255
+ containerClassName="md:left-[4.75rem]"
256
+ >
257
+ <SettingsPanel />
258
+ </Dialog>
259
+ ```
260
+
261
+ ## Migration from CenteredDialog / ResponsiveSheet
262
+
263
+ `CenteredDialog` and `ResponsiveSheet` were removed in 0.16.x.
264
+
265
+ ```tsx
266
+ // Before
267
+ <CenteredDialog visible={v} onClose={c}>…</CenteredDialog>
268
+ // After
269
+ <Dialog placement="center" open={v} onClose={c}>…</Dialog>
270
+
271
+ // Before
272
+ <ResponsiveSheet side="left" open={o} onClose={c}>…</ResponsiveSheet>
273
+ // After
274
+ <Dialog placement={{ base: 'bottom', md: 'left' }} open={o} onClose={c}>…</Dialog>
275
+ ```
276
+
277
+ Removed exports: `CenteredDialog`, `CenteredDialogProps`, `BLOOM_CENTERED_DIALOG_CSS`, `CENTERED_DIALOG_BACKDROP_TESTID`, `ResponsiveSheet`, `@oxyhq/bloom/responsive-sheet` subpath.
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: Divider
3
+ description: The one separator. Nothing else in the library draws a rule by hand.
4
+ order: 27
5
+ ---
6
+
7
+ # Divider
8
+
9
+ A hairline in the `border` role. Three families used to draw their own 1px
10
+ separator and each had a slightly different colour — that is the whole reason
11
+ this component exists.
12
+
13
+ ```tsx
14
+ import { Divider } from '@oxyhq/bloom/divider';
15
+
16
+ <Divider />
17
+ <Divider spacing={12} />
18
+ <Divider vertical spacing={12} />
19
+ ```
20
+
21
+ ## Which prop decides what
22
+
23
+ | prop | decides |
24
+ | ----------- | ------------------------------------------------------------ |
25
+ | `vertical` | the axis |
26
+ | `spacing` | margin on the axis it separates — the prop you usually want |
27
+ | `thickness` | the weight; defaults to the platform hairline |
28
+ | `color` | an override; defaults to the `border` role |
29
+
30
+ Reach for `spacing` first. Most "the divider looks wrong" cases are margin, not
31
+ weight — a rule pressed against the text above it reads as heavier than it is.
32
+
33
+ ## A vertical divider needs a parent with a height
34
+
35
+ It fills the cross axis (`alignSelf: 'stretch'`); it does not invent a height.
36
+ In a row with no height it is invisible rather than broken — which is the usual
37
+ reason a toolbar rule "doesn't render".
38
+
39
+ ```tsx
40
+ <View style={{ flexDirection: 'row', alignItems: 'center', height: 40 }}>
41
+ <Text>Drafts</Text>
42
+ <Divider vertical spacing={12} />
43
+ <Text>Sent</Text>
44
+ </View>
45
+ ```
46
+
47
+ ## When not to use one
48
+
49
+ A divider between every row of a list is usually noise: a settings group already
50
+ separates itself with its card, and rows separated by space read as a group
51
+ without a rule. Use one where a boundary is *meaningful* — between sections,
52
+ not between items.
53
+
54
+ `SettingsListDivider` is the inset variant for grouped rows; prefer it inside a
55
+ settings group so the rule starts after the icon column instead of cutting the
56
+ card in half.