@oxy.so/bloom 7.1.5 → 7.3.0

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 (286) hide show
  1. package/docs/adoption-matrix.mdx +18 -17
  2. package/docs/app-shell.mdx +48 -2
  3. package/docs/appearance.mdx +16 -1
  4. package/docs/chip.mdx +5 -0
  5. package/docs/content-panel.mdx +9 -0
  6. package/docs/error-boundary.mdx +35 -0
  7. package/docs/mail-thread.mdx +8 -0
  8. package/docs/page-footer.mdx +60 -0
  9. package/docs/settings-modal.mdx +5 -0
  10. package/docs/sidebar.mdx +1 -1
  11. package/lib/commonjs/app-shell/AppShell.js +3 -2
  12. package/lib/commonjs/app-shell/AppShell.js.map +1 -1
  13. package/lib/commonjs/app-shell/AppShellBars.js +8 -5
  14. package/lib/commonjs/app-shell/AppShellBars.js.map +1 -1
  15. package/lib/commonjs/app-shell/AppShellEngine.js +81 -36
  16. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  17. package/lib/commonjs/app-shell/AppShellSplit.js +26 -14
  18. package/lib/commonjs/app-shell/AppShellSplit.js.map +1 -1
  19. package/lib/commonjs/appearance/BloomScope.js +8 -3
  20. package/lib/commonjs/appearance/BloomScope.js.map +1 -1
  21. package/lib/commonjs/appearance/context.js.map +1 -1
  22. package/lib/commonjs/chip/Chip.js +51 -7
  23. package/lib/commonjs/chip/Chip.js.map +1 -1
  24. package/lib/commonjs/content-panel/ContentPanel.js +40 -24
  25. package/lib/commonjs/content-panel/ContentPanel.js.map +1 -1
  26. package/lib/commonjs/content-panel/ContentPanel.web.js +45 -18
  27. package/lib/commonjs/content-panel/ContentPanel.web.js.map +1 -1
  28. package/lib/commonjs/design-tokens/shapes.js +2 -2
  29. package/lib/commonjs/design-tokens/shapes.js.map +1 -1
  30. package/lib/commonjs/design-tokens/tokens.json +2 -2
  31. package/lib/commonjs/error-boundary/ErrorBoundary.js +10 -0
  32. package/lib/commonjs/error-boundary/ErrorBoundary.js.map +1 -1
  33. package/lib/commonjs/error-boundary/PanelErrorBoundary.js +64 -0
  34. package/lib/commonjs/error-boundary/PanelErrorBoundary.js.map +1 -0
  35. package/lib/commonjs/error-boundary/index.js +7 -0
  36. package/lib/commonjs/error-boundary/index.js.map +1 -1
  37. package/lib/commonjs/index.js +946 -927
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/index.web.js +946 -927
  40. package/lib/commonjs/index.web.js.map +1 -1
  41. package/lib/commonjs/mail-thread/MailMessage.js +8 -14
  42. package/lib/commonjs/mail-thread/MailMessage.js.map +1 -1
  43. package/lib/commonjs/mail-thread/MailThread.js +6 -2
  44. package/lib/commonjs/mail-thread/MailThread.js.map +1 -1
  45. package/lib/commonjs/mail-thread/shared.js +2 -1
  46. package/lib/commonjs/mail-thread/shared.js.map +1 -1
  47. package/lib/commonjs/page-footer/PageFooter.js +137 -0
  48. package/lib/commonjs/page-footer/PageFooter.js.map +1 -0
  49. package/lib/commonjs/page-footer/PageFooterProvider.js +22 -0
  50. package/lib/commonjs/page-footer/PageFooterProvider.js.map +1 -0
  51. package/lib/commonjs/page-footer/context.js +11 -0
  52. package/lib/commonjs/page-footer/context.js.map +1 -0
  53. package/lib/commonjs/page-footer/index.js +27 -0
  54. package/lib/commonjs/page-footer/index.js.map +1 -0
  55. package/lib/commonjs/page-footer/types.js +6 -0
  56. package/lib/commonjs/page-footer/types.js.map +1 -0
  57. package/lib/commonjs/page-footer/use-page-footer-inset.js +19 -0
  58. package/lib/commonjs/page-footer/use-page-footer-inset.js.map +1 -0
  59. package/lib/commonjs/settings-modal/SettingsPlanCard.js +11 -7
  60. package/lib/commonjs/settings-modal/SettingsPlanCard.js.map +1 -1
  61. package/lib/commonjs/sidebar/Sidebar.js +6 -5
  62. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  63. package/lib/commonjs/styles/panel-chrome.js +15 -1
  64. package/lib/commonjs/styles/panel-chrome.js.map +1 -1
  65. package/lib/module/app-shell/AppShell.js +3 -2
  66. package/lib/module/app-shell/AppShell.js.map +1 -1
  67. package/lib/module/app-shell/AppShellBars.js +8 -5
  68. package/lib/module/app-shell/AppShellBars.js.map +1 -1
  69. package/lib/module/app-shell/AppShellEngine.js +81 -36
  70. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  71. package/lib/module/app-shell/AppShellSplit.js +26 -14
  72. package/lib/module/app-shell/AppShellSplit.js.map +1 -1
  73. package/lib/module/appearance/BloomScope.js +8 -3
  74. package/lib/module/appearance/BloomScope.js.map +1 -1
  75. package/lib/module/appearance/context.js.map +1 -1
  76. package/lib/module/chip/Chip.js +51 -7
  77. package/lib/module/chip/Chip.js.map +1 -1
  78. package/lib/module/content-panel/ContentPanel.js +41 -25
  79. package/lib/module/content-panel/ContentPanel.js.map +1 -1
  80. package/lib/module/content-panel/ContentPanel.web.js +47 -20
  81. package/lib/module/content-panel/ContentPanel.web.js.map +1 -1
  82. package/lib/module/design-tokens/shapes.js +2 -2
  83. package/lib/module/design-tokens/shapes.js.map +1 -1
  84. package/lib/module/design-tokens/tokens.json +2 -2
  85. package/lib/module/error-boundary/ErrorBoundary.js +10 -0
  86. package/lib/module/error-boundary/ErrorBoundary.js.map +1 -1
  87. package/lib/module/error-boundary/PanelErrorBoundary.js +59 -0
  88. package/lib/module/error-boundary/PanelErrorBoundary.js.map +1 -0
  89. package/lib/module/error-boundary/index.js +1 -0
  90. package/lib/module/error-boundary/index.js.map +1 -1
  91. package/lib/module/index.js +2 -1
  92. package/lib/module/index.js.map +1 -1
  93. package/lib/module/index.web.js +2 -1
  94. package/lib/module/index.web.js.map +1 -1
  95. package/lib/module/mail-thread/MailMessage.js +8 -14
  96. package/lib/module/mail-thread/MailMessage.js.map +1 -1
  97. package/lib/module/mail-thread/MailThread.js +6 -2
  98. package/lib/module/mail-thread/MailThread.js.map +1 -1
  99. package/lib/module/mail-thread/shared.js +2 -1
  100. package/lib/module/mail-thread/shared.js.map +1 -1
  101. package/lib/module/page-footer/PageFooter.js +132 -0
  102. package/lib/module/page-footer/PageFooter.js.map +1 -0
  103. package/lib/module/page-footer/PageFooterProvider.js +18 -0
  104. package/lib/module/page-footer/PageFooterProvider.js.map +1 -0
  105. package/lib/module/page-footer/context.js +7 -0
  106. package/lib/module/page-footer/context.js.map +1 -0
  107. package/lib/module/page-footer/index.js +6 -0
  108. package/lib/module/page-footer/index.js.map +1 -0
  109. package/lib/module/page-footer/types.js +4 -0
  110. package/lib/module/page-footer/types.js.map +1 -0
  111. package/lib/module/page-footer/use-page-footer-inset.js +16 -0
  112. package/lib/module/page-footer/use-page-footer-inset.js.map +1 -0
  113. package/lib/module/settings-modal/SettingsPlanCard.js +7 -3
  114. package/lib/module/settings-modal/SettingsPlanCard.js.map +1 -1
  115. package/lib/module/sidebar/Sidebar.js +6 -5
  116. package/lib/module/sidebar/Sidebar.js.map +1 -1
  117. package/lib/module/styles/panel-chrome.js +14 -1
  118. package/lib/module/styles/panel-chrome.js.map +1 -1
  119. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts +4 -0
  120. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/app-shell/AppShellSplit.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/app-shell/types.d.ts +16 -3
  124. package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/appearance/BloomScope.d.ts +1 -1
  126. package/lib/typescript/commonjs/appearance/BloomScope.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/appearance/context.d.ts +2 -2
  128. package/lib/typescript/commonjs/appearance/context.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/appearance/types.d.ts +4 -1
  130. package/lib/typescript/commonjs/appearance/types.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts +3 -4
  133. package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts +1 -1
  135. package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/content-panel/types.d.ts +5 -2
  137. package/lib/typescript/commonjs/content-panel/types.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/design-tokens/shapes.d.ts +2 -2
  139. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts +1 -0
  140. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/error-boundary/PanelErrorBoundary.d.ts +4 -0
  142. package/lib/typescript/commonjs/error-boundary/PanelErrorBoundary.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/error-boundary/index.d.ts +2 -1
  144. package/lib/typescript/commonjs/error-boundary/index.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/error-boundary/types.d.ts +16 -0
  146. package/lib/typescript/commonjs/error-boundary/types.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/index.d.ts +3 -2
  148. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/index.web.d.ts +3 -2
  150. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/mail-thread/MailMessage.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/mail-thread/MailThread.d.ts +1 -1
  153. package/lib/typescript/commonjs/mail-thread/MailThread.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/mail-thread/shared.d.ts +1 -1
  155. package/lib/typescript/commonjs/mail-thread/shared.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/mail-thread/types.d.ts +2 -0
  157. package/lib/typescript/commonjs/mail-thread/types.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/page-footer/PageFooter.d.ts +7 -0
  159. package/lib/typescript/commonjs/page-footer/PageFooter.d.ts.map +1 -0
  160. package/lib/typescript/commonjs/page-footer/PageFooterProvider.d.ts +4 -0
  161. package/lib/typescript/commonjs/page-footer/PageFooterProvider.d.ts.map +1 -0
  162. package/lib/typescript/commonjs/page-footer/context.d.ts +3 -0
  163. package/lib/typescript/commonjs/page-footer/context.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/page-footer/index.d.ts +5 -0
  165. package/lib/typescript/commonjs/page-footer/index.d.ts.map +1 -0
  166. package/lib/typescript/commonjs/page-footer/types.d.ts +26 -0
  167. package/lib/typescript/commonjs/page-footer/types.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/page-footer/use-page-footer-inset.d.ts +7 -0
  169. package/lib/typescript/commonjs/page-footer/use-page-footer-inset.d.ts.map +1 -0
  170. package/lib/typescript/commonjs/settings-modal/SettingsPlanCard.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/settings-modal/types.d.ts +3 -0
  172. package/lib/typescript/commonjs/settings-modal/types.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/sidebar/types.d.ts +1 -1
  175. package/lib/typescript/commonjs/styles/panel-chrome.d.ts +5 -0
  176. package/lib/typescript/commonjs/styles/panel-chrome.d.ts.map +1 -1
  177. package/lib/typescript/module/app-shell/AppShellBars.d.ts +4 -0
  178. package/lib/typescript/module/app-shell/AppShellBars.d.ts.map +1 -1
  179. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  180. package/lib/typescript/module/app-shell/AppShellSplit.d.ts.map +1 -1
  181. package/lib/typescript/module/app-shell/types.d.ts +16 -3
  182. package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
  183. package/lib/typescript/module/appearance/BloomScope.d.ts +1 -1
  184. package/lib/typescript/module/appearance/BloomScope.d.ts.map +1 -1
  185. package/lib/typescript/module/appearance/context.d.ts +2 -2
  186. package/lib/typescript/module/appearance/context.d.ts.map +1 -1
  187. package/lib/typescript/module/appearance/types.d.ts +4 -1
  188. package/lib/typescript/module/appearance/types.d.ts.map +1 -1
  189. package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
  190. package/lib/typescript/module/content-panel/ContentPanel.d.ts +3 -4
  191. package/lib/typescript/module/content-panel/ContentPanel.d.ts.map +1 -1
  192. package/lib/typescript/module/content-panel/ContentPanel.web.d.ts +1 -1
  193. package/lib/typescript/module/content-panel/ContentPanel.web.d.ts.map +1 -1
  194. package/lib/typescript/module/content-panel/types.d.ts +5 -2
  195. package/lib/typescript/module/content-panel/types.d.ts.map +1 -1
  196. package/lib/typescript/module/design-tokens/shapes.d.ts +2 -2
  197. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts +1 -0
  198. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts.map +1 -1
  199. package/lib/typescript/module/error-boundary/PanelErrorBoundary.d.ts +4 -0
  200. package/lib/typescript/module/error-boundary/PanelErrorBoundary.d.ts.map +1 -0
  201. package/lib/typescript/module/error-boundary/index.d.ts +2 -1
  202. package/lib/typescript/module/error-boundary/index.d.ts.map +1 -1
  203. package/lib/typescript/module/error-boundary/types.d.ts +16 -0
  204. package/lib/typescript/module/error-boundary/types.d.ts.map +1 -1
  205. package/lib/typescript/module/index.d.ts +3 -2
  206. package/lib/typescript/module/index.d.ts.map +1 -1
  207. package/lib/typescript/module/index.web.d.ts +3 -2
  208. package/lib/typescript/module/index.web.d.ts.map +1 -1
  209. package/lib/typescript/module/mail-thread/MailMessage.d.ts.map +1 -1
  210. package/lib/typescript/module/mail-thread/MailThread.d.ts +1 -1
  211. package/lib/typescript/module/mail-thread/MailThread.d.ts.map +1 -1
  212. package/lib/typescript/module/mail-thread/shared.d.ts +1 -1
  213. package/lib/typescript/module/mail-thread/shared.d.ts.map +1 -1
  214. package/lib/typescript/module/mail-thread/types.d.ts +2 -0
  215. package/lib/typescript/module/mail-thread/types.d.ts.map +1 -1
  216. package/lib/typescript/module/page-footer/PageFooter.d.ts +7 -0
  217. package/lib/typescript/module/page-footer/PageFooter.d.ts.map +1 -0
  218. package/lib/typescript/module/page-footer/PageFooterProvider.d.ts +4 -0
  219. package/lib/typescript/module/page-footer/PageFooterProvider.d.ts.map +1 -0
  220. package/lib/typescript/module/page-footer/context.d.ts +3 -0
  221. package/lib/typescript/module/page-footer/context.d.ts.map +1 -0
  222. package/lib/typescript/module/page-footer/index.d.ts +5 -0
  223. package/lib/typescript/module/page-footer/index.d.ts.map +1 -0
  224. package/lib/typescript/module/page-footer/types.d.ts +26 -0
  225. package/lib/typescript/module/page-footer/types.d.ts.map +1 -0
  226. package/lib/typescript/module/page-footer/use-page-footer-inset.d.ts +7 -0
  227. package/lib/typescript/module/page-footer/use-page-footer-inset.d.ts.map +1 -0
  228. package/lib/typescript/module/settings-modal/SettingsPlanCard.d.ts.map +1 -1
  229. package/lib/typescript/module/settings-modal/types.d.ts +3 -0
  230. package/lib/typescript/module/settings-modal/types.d.ts.map +1 -1
  231. package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
  232. package/lib/typescript/module/sidebar/types.d.ts +1 -1
  233. package/lib/typescript/module/styles/panel-chrome.d.ts +5 -0
  234. package/lib/typescript/module/styles/panel-chrome.d.ts.map +1 -1
  235. package/package.json +18 -1
  236. package/src/__tests__/support/adoption-matrix.ts +683 -0
  237. package/src/__tests__/support/card-surface.ts +25 -0
  238. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  239. package/src/__tests__/support/commerce-harness.tsx +97 -0
  240. package/src/__tests__/support/composite.ts +68 -0
  241. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  242. package/src/__tests__/support/messages-in.ts +10 -0
  243. package/src/__tests__/support/press-host.ts +30 -0
  244. package/src/__tests__/support/rendered-style.ts +99 -0
  245. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  246. package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
  247. package/src/agent-avatar/__tests__/artwork-golden.json +774 -0
  248. package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +26 -0
  249. package/src/app-shell/AppShell.tsx +2 -2
  250. package/src/app-shell/AppShellBars.tsx +6 -4
  251. package/src/app-shell/AppShellEngine.tsx +51 -18
  252. package/src/app-shell/AppShellSplit.tsx +16 -7
  253. package/src/app-shell/types.ts +16 -3
  254. package/src/appearance/BloomScope.tsx +5 -2
  255. package/src/appearance/context.ts +2 -2
  256. package/src/appearance/types.ts +4 -1
  257. package/src/chip/Chip.tsx +26 -5
  258. package/src/content-panel/ContentPanel.tsx +35 -26
  259. package/src/content-panel/ContentPanel.web.tsx +35 -24
  260. package/src/content-panel/types.ts +5 -2
  261. package/src/design-tokens/shapes.ts +1 -1
  262. package/src/design-tokens/tokens.json +2 -2
  263. package/src/error-boundary/ErrorBoundary.tsx +6 -0
  264. package/src/error-boundary/PanelErrorBoundary.tsx +38 -0
  265. package/src/error-boundary/index.ts +3 -0
  266. package/src/error-boundary/types.ts +18 -0
  267. package/src/index.ts +4 -1
  268. package/src/index.web.ts +4 -1
  269. package/src/mail-thread/MailMessage.tsx +9 -11
  270. package/src/mail-thread/MailThread.tsx +3 -1
  271. package/src/mail-thread/shared.ts +2 -1
  272. package/src/mail-thread/types.ts +2 -0
  273. package/src/page-footer/PageFooter.tsx +96 -0
  274. package/src/page-footer/PageFooterProvider.tsx +9 -0
  275. package/src/page-footer/context.ts +6 -0
  276. package/src/page-footer/index.ts +4 -0
  277. package/src/page-footer/types.ts +26 -0
  278. package/src/page-footer/use-page-footer-inset.ts +17 -0
  279. package/src/settings-modal/SettingsPlanCard.tsx +2 -0
  280. package/src/settings-modal/types.ts +3 -0
  281. package/src/sidebar/Sidebar.tsx +5 -4
  282. package/src/sidebar/types.ts +1 -1
  283. package/src/styles/panel-chrome.ts +9 -1
  284. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
  285. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
  286. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
@@ -1,3 +1,4 @@
1
+ import { PanelErrorBoundary } from '../error-boundary/PanelErrorBoundary';
1
2
  import { resolveSurfaceMaterial } from '../surface/resolve-surface-material';
2
3
  /**
3
4
  * Native variant of `ContentPanel` — the framed app-content surface.
@@ -11,13 +12,12 @@ import { resolveSurfaceMaterial } from '../surface/resolve-surface-material';
11
12
  * On NATIVE none of the WEB overlay machinery applies: there is no document
12
13
  * scroll, no sticky positioning, and no bleed to mask, so the panel is simply a
13
14
  * surface wrapping its content. The `framed` prop still drives whether that
14
- * surface is rounded + bordered, tri-state and resolved with pure NativeWind
15
- * (the breakpoint's `min-*:` variant evaluates against the window width on
16
- * native too — the material follows the same window breakpoint):
15
+ * surface is rounded + bordered, tri-state with responsive NativeWind borders
16
+ * (the material and scoped radius follow the same window breakpoint):
17
17
  *
18
18
  * - `undefined` (DEFAULT) → responsive: full-bleed below the `framedFrom`
19
19
  * breakpoint (default `768` / Tailwind `md`), rounded + bordered at/above it
20
- * (e.g. `md:rounded-radius-28 md:border md:border-border`).
20
+ * (e.g. `md:border md:border-border`).
21
21
  * - `false` → never framed (plain full-bleed at every size).
22
22
  * - `true` → always rounded + bordered.
23
23
  *
@@ -35,7 +35,8 @@ import React, { memo } from 'react';
35
35
  import { SurfacePaint } from '../surface/SurfacePaint';
36
36
  import { StyleSheet, useWindowDimensions } from 'react-native';
37
37
 
38
- import { useOptionalPanelChrome } from '../styles/panel-chrome';
38
+ import { useOptionalPanelChrome, usePanelShape } from '../styles/panel-chrome';
39
+ import { surfaceStyle as resolveShapeStyle } from '../shapes/surface-style';
39
40
  import { StyledView } from '../styles/styled-primitives';
40
41
  import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill } from '../styles/surface-levels';
41
42
  import {
@@ -63,21 +64,22 @@ export const PANEL_BOTTOM_INSET = 8;
63
64
  * value is the only runtime decision.
64
65
  */
65
66
  const RESPONSIVE_SURFACE: Record<ContentPanelFramedBreakpoint, string> = {
66
- 500: 'min-[500px]:rounded-radius-28 min-[500px]:border min-[500px]:border-border',
67
- 640: 'sm:rounded-radius-28 sm:border sm:border-border',
68
- 768: 'md:rounded-radius-28 md:border md:border-border',
69
- 1024: 'lg:rounded-radius-28 lg:border lg:border-border',
67
+ 500: 'min-[500px]:border min-[500px]:border-border',
68
+ 640: 'sm:border sm:border-border',
69
+ 768: 'md:border md:border-border',
70
+ 1024: 'lg:border lg:border-border',
70
71
  };
71
72
 
72
73
  const RESPONSIVE_CONTENT: Record<ContentPanelFramedBreakpoint, string> = {
73
- 500: 'min-[500px]:overflow-hidden min-[500px]:rounded-radius-28',
74
- 640: 'sm:overflow-hidden sm:rounded-radius-28',
75
- 768: 'md:overflow-hidden md:rounded-radius-28',
76
- 1024: 'lg:overflow-hidden lg:rounded-radius-28',
74
+ 500: 'min-[500px]:overflow-hidden',
75
+ 640: 'sm:overflow-hidden',
76
+ 768: 'md:overflow-hidden',
77
+ 1024: 'lg:overflow-hidden',
77
78
  };
78
79
 
79
80
  const ContentPanelComponent: React.FC<ContentPanelProps> = ({
80
81
  children,
82
+ errorBoundary = false,
81
83
  framed,
82
84
  framedFrom = 768,
83
85
  fill = false,
@@ -91,33 +93,38 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
91
93
  }) => {
92
94
  // Dev-only invariant — a ContentPanel must never be nested inside another.
93
95
  useContentPanelNestingGuard();
96
+ const panelShape = usePanelShape();
94
97
  const panelChrome = useOptionalPanelChrome();
95
98
  // What the panel tells its subtree it is painted in (`./shared.ts`).
96
99
  const rawFill = usePanelSurfaceFill(surfaceClassName, surfaceStyle, surfaceColor);
97
100
  const defaultFill = rawFill;
98
101
  const { width } = useWindowDimensions();
99
102
  const parentFill = useOptionalSurfaceFill();
100
- const paintsSurface = Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom);
103
+ const paintsSurface = Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom) && chrome !== 'none';
101
104
  const publishedFill = rawFill && paintsSurface ? resolveSurfaceMaterial({ fill: rawFill, parentFill: parentFill! }).publishedFill : rawFill;
102
- const radius = StyleSheet.flatten(surfaceStyle)?.borderRadius ?? 28;
105
+ const radius = StyleSheet.flatten(surfaceStyle)?.borderRadius ?? panelShape.radius;
106
+ const isFramed = framed ?? width >= framedFrom;
107
+ const geometry = { ...resolveShapeStyle({ curve: panelShape.curve }), borderRadius: isFramed ? radius : 0 };
103
108
 
104
109
  // Tri-state: `undefined` → responsive (breakpoint-gated), `true` → always
105
110
  // framed, `false` → never framed (plain full-bleed). Whole literal class
106
111
  // strings are selected per mode so the Tailwind content-scan over `src/**`
107
- // picks up every `min-*`/`sm:`/`md:`/`lg:` / `rounded-radius-28` token verbatim
112
+ // picks up every `min-*`/`sm:`/`md:`/`lg:` token verbatim
108
113
  // (the breakpoint bundle lives whole in `RESPONSIVE_SURFACE`; `framed ===
109
114
  // true`/`false` ignore `framedFrom` for fixed always/never framing).
110
115
  const surfaceBase =
111
- framed === undefined
112
- ? `flex-1 ${RESPONSIVE_SURFACE[framedFrom]}`
113
- : framed
114
- ? 'flex-1 rounded-radius-28 border border-border'
115
- : 'flex-1';
116
+ chrome === 'none'
117
+ ? 'flex-1'
118
+ : framed === undefined
119
+ ? `flex-1 ${RESPONSIVE_SURFACE[framedFrom]}`
120
+ : framed
121
+ ? 'flex-1 border border-border'
122
+ : 'flex-1';
116
123
  const surfaceClass = [surfaceBase, fill ? 'min-h-0' : '', surfaceClassName ?? (paintsSurface ? 'bg-transparent' : 'bg-card')].filter(Boolean).join(' ');
117
124
  // Native has no overlays: the surface itself carries the edge. `none` drops
118
125
  // both, `border` keeps the class-drawn hairline, `elevated` adds the lift.
119
126
  const chromeStyle =
120
- framed === false || chrome === 'none' || chrome === 'border'
127
+ !isFramed || chrome === 'none' || chrome === 'border'
121
128
  ? null
122
129
  : shadow || panelChrome
123
130
  ? { boxShadow: shadow ?? panelChrome?.shadow }
@@ -127,28 +134,30 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
127
134
  // bottom and a list inside it scrolls within it. What the prop adds here is
128
135
  // `min-h-0`, so a tall child cannot push the panel past the box it was given
129
136
  // — the same automatic-minimum-size trap as on web.
130
- const contentClass = ['flex-1', framed === true ? 'overflow-hidden rounded-radius-28' : framed === undefined ? RESPONSIVE_CONTENT[framedFrom] : '', fill ? 'min-h-0' : '', contentClassName].filter(Boolean).join(' ');
137
+ const contentClass = ['flex-1', framed === true ? 'overflow-hidden' : framed === undefined ? RESPONSIVE_CONTENT[framedFrom] : '', fill ? 'min-h-0' : '', contentClassName].filter(Boolean).join(' ');
131
138
 
132
139
  return (
133
140
  <ContentPanelNestingContext.Provider value={true}>
134
141
  <StyledView
142
+ testID="content-panel-surface"
135
143
  className={surfaceClass}
136
144
  style={[
137
145
  // The surface variable rides the element that carries the fill, so
138
146
  // the two can never disagree. A no-op on native; the provider below
139
147
  // is what answers there. (`styles/surface-levels.ts`.)
140
148
  surfaceFillVars(publishedFill),
149
+ geometry,
141
150
  chromeStyle,
142
151
  surfaceStyle,
143
152
  paintsSurface ? { backgroundColor: 'transparent' } : null,
144
153
  ]}
145
154
  >
146
- {paintsSurface ? <SurfacePaint fill={defaultFill!} radius={radius} /> : null}
147
- <StyledView className={contentClass} style={[{ borderRadius: (framed ?? width >= framedFrom) ? radius : 0 }, contentStyle]}>
155
+ {paintsSurface ? <SurfacePaint fill={defaultFill!} radius={radius} shape={{ curve: panelShape.curve }} /> : null}
156
+ <StyledView testID="content-panel-content" className={contentClass} style={[geometry, contentStyle]}>
148
157
  {/* The panel is a surface: everything inside is sitting on rung 1,
149
158
  painted in the colour this panel actually paints. */}
150
159
  <SurfaceLevelProvider level={1} fill={publishedFill}>
151
- {children}
160
+ {errorBoundary === false ? children : <PanelErrorBoundary {...(errorBoundary === true ? {} : errorBoundary)}>{children}</PanelErrorBoundary>}
152
161
  </SurfaceLevelProvider>
153
162
  </StyledView>
154
163
  </StyledView>
@@ -1,3 +1,4 @@
1
+ import { PanelErrorBoundary } from '../error-boundary/PanelErrorBoundary';
1
2
  import { resolveSurfaceMaterial } from '../surface/resolve-surface-material';
2
3
  /**
3
4
  * Web variant of `ContentPanel` — the framed app-content surface.
@@ -83,18 +84,19 @@ import { resolveSurfaceMaterial } from '../surface/resolve-surface-material';
83
84
  *
84
85
  * Styling is NativeWind-className-first; the literal class strings below MUST
85
86
  * stay literal so a consumer's Tailwind content-scan over `lib/**` picks them up
86
- * (no dynamic concatenation of the arbitrary `web:[…]` / `rounded-radius-28` /
87
+ * (no dynamic concatenation of the arbitrary `web:[…]` / `rounded-[var(--bloom-panel-radius)]` /
87
88
  * `md:` parts — whole class strings are selected per mode instead).
88
89
  */
89
90
  import React, { memo, useInsertionEffect } from 'react';
90
91
  import { useSurfaceMaterial } from '../surface/use-surface-material.web';
91
- import { useWindowDimensions, type ViewStyle } from 'react-native';
92
+ import { StyleSheet, useWindowDimensions, type ViewStyle } from 'react-native';
92
93
 
93
94
  import { adoptStyleSheet } from '../styles/adopt-style-sheet';
94
95
  import type { WebCssStyle } from '../styles/web-view-style';
96
+ import { surfaceStyle as resolveShapeStyle } from '../shapes/surface-style';
95
97
  import { StyledView } from '../styles/styled-primitives';
96
98
 
97
- import { useOptionalPanelChrome } from '../styles/panel-chrome';
99
+ import { useOptionalPanelChrome, usePanelShape } from '../styles/panel-chrome';
98
100
  import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill } from '../styles/surface-levels';
99
101
  import { useTheme } from '../theme/use-theme';
100
102
  import {
@@ -130,23 +132,23 @@ const RESPONSIVE_WEB: Record<
130
132
  { surface: string; content: string; overlayHidden: string }
131
133
  > = {
132
134
  500: {
133
- surface: 'min-[500px]:rounded-radius-28',
134
- content: 'min-[500px]:rounded-radius-28 web:min-[500px]:overflow-x-clip',
135
+ surface: 'min-[500px]:rounded-[var(--bloom-panel-radius)]',
136
+ content: 'min-[500px]:rounded-[var(--bloom-panel-radius)] web:min-[500px]:overflow-x-clip',
135
137
  overlayHidden: 'max-[500px]:hidden',
136
138
  },
137
139
  640: {
138
- surface: 'sm:rounded-radius-28',
139
- content: 'sm:rounded-radius-28 web:sm:overflow-x-clip',
140
+ surface: 'sm:rounded-[var(--bloom-panel-radius)]',
141
+ content: 'sm:rounded-[var(--bloom-panel-radius)] web:sm:overflow-x-clip',
140
142
  overlayHidden: 'max-sm:hidden',
141
143
  },
142
144
  768: {
143
- surface: 'md:rounded-radius-28',
144
- content: 'md:rounded-radius-28 web:md:overflow-x-clip',
145
+ surface: 'md:rounded-[var(--bloom-panel-radius)]',
146
+ content: 'md:rounded-[var(--bloom-panel-radius)] web:md:overflow-x-clip',
145
147
  overlayHidden: 'max-md:hidden',
146
148
  },
147
149
  1024: {
148
- surface: 'lg:rounded-radius-28',
149
- content: 'lg:rounded-radius-28 web:lg:overflow-x-clip',
150
+ surface: 'lg:rounded-[var(--bloom-panel-radius)]',
151
+ content: 'lg:rounded-[var(--bloom-panel-radius)] web:lg:overflow-x-clip',
150
152
  overlayHidden: 'max-lg:hidden',
151
153
  },
152
154
  };
@@ -170,6 +172,7 @@ const PANEL_INSET_CSS = `
170
172
 
171
173
  const ContentPanelComponent: React.FC<ContentPanelProps> = ({
172
174
  children,
175
+ errorBoundary = false,
173
176
  framed,
174
177
  framedFrom = 768,
175
178
  fill = false,
@@ -197,13 +200,18 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
197
200
  // The floating-panel edge the `Sidebar` wears. Optional: the panel drew its
198
201
  // own surface long before it had a shadow, so it must not start throwing
199
202
  // outside a provider.
203
+ const panelShape = usePanelShape();
204
+ const radius = StyleSheet.flatten(surfaceStyle)?.borderRadius ?? panelShape.radius;
205
+ const curveStyle = resolveShapeStyle({ curve: panelShape.curve });
206
+ const panelStyle = { ...curveStyle, borderRadius: radius };
207
+ const panelVars: WebCssStyle = { '--bloom-panel-radius': typeof radius === 'string' ? radius : `${typeof radius === 'number' ? radius : panelShape.radius}px` };
200
208
  const panelChrome = useOptionalPanelChrome();
201
209
  // What the panel tells its subtree it is painted in (`./shared.ts`).
202
210
  const rawFill = usePanelSurfaceFill(surfaceClassName, surfaceStyle, surfaceColor);
203
211
  const defaultFill = rawFill;
204
212
  const { width } = useWindowDimensions();
205
213
  const parentFill = useOptionalSurfaceFill();
206
- const paintsSurface = Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom);
214
+ const paintsSurface = Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom) && chrome !== 'none';
207
215
  const publishedFill = rawFill && paintsSurface ? resolveSurfaceMaterial({ fill: rawFill, parentFill: parentFill! }).publishedFill : rawFill;
208
216
  const materialStyle = useSurfaceMaterial('[data-bloom-panel-material]', 'bloom-content-panel-material', defaultFill ?? 'transparent');
209
217
 
@@ -223,12 +231,12 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
223
231
  const surfaceBase = responsive
224
232
  ? `flex-1 ${bp.surface}`
225
233
  : framed
226
- ? 'flex-1 rounded-radius-28'
234
+ ? 'flex-1 rounded-[var(--bloom-panel-radius)]'
227
235
  : 'flex-1';
228
236
  const contentBase = responsive
229
237
  ? `flex-1 ${bp.content}`
230
238
  : framed
231
- ? 'flex-1 rounded-radius-28 web:overflow-x-clip'
239
+ ? 'flex-1 rounded-[var(--bloom-panel-radius)] web:overflow-x-clip'
232
240
  : 'flex-1';
233
241
  // `fill`: the panel takes the height its parent gives it and the CONTENT
234
242
  // scrolls inside. `min-h-0` on both boxes is the whole trick — a flex child's
@@ -300,6 +308,9 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
300
308
  // disagree about what the panel painted.
301
309
  surfaceFillVars(publishedFill),
302
310
  insetVars,
311
+ panelVars,
312
+ curveStyle,
313
+ framed === true ? { borderRadius: radius } : framed === false ? { borderRadius: 0 } : null,
303
314
  surfaceStyle,
304
315
  paintsSurface ? materialStyle : null,
305
316
  ]}
@@ -315,12 +326,12 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
315
326
  pointerEvents="none"
316
327
  className={
317
328
  boundToPanel
318
- ? `web:[grid-area:1/1] z-30 h-full w-full rounded-radius-28 ${responsive ? bp.overlayHidden : ''} web:[clip-path:inset(0)]`
329
+ ? `web:[grid-area:1/1] z-30 h-full w-full rounded-[var(--bloom-panel-radius)] ${responsive ? bp.overlayHidden : ''} web:[clip-path:inset(0)]`
319
330
  : responsive
320
- ? `web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-radius-28 ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
321
- : 'web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-radius-28 web:[margin-bottom:calc(-100dvh+16px)]'
331
+ ? `web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
332
+ : 'web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] web:[margin-bottom:calc(-100dvh+16px)]'
322
333
  }
323
- style={{ ...insetStyle, boxShadow: `0 0 0 ${maskSpread}px ${maskColor ?? colors.background}` }}
334
+ style={{ ...panelStyle, ...insetStyle, boxShadow: `0 0 0 ${maskSpread}px ${maskColor ?? colors.background}` }}
324
335
  />
325
336
  )}
326
337
  {/* (2) Border-frame overlay — one continuous rounded border, above all.
@@ -333,15 +344,15 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
333
344
  // The shadow rides the SAME element as the hairline, so the lift and
334
345
  // the edge can never disagree about where the panel ends. It paints
335
346
  // outward, into the gutter the page background shows.
336
- style={[insetStyle, chrome === 'elevated' && (shadow || panelChrome)
347
+ style={[panelStyle, insetStyle, chrome === 'elevated' && (shadow || panelChrome)
337
348
  ? { boxShadow: shadow ?? panelChrome?.shadow }
338
349
  : null]}
339
350
  className={
340
351
  boundToPanel
341
- ? `web:[grid-area:1/1] z-[120] h-full w-full rounded-radius-28 border border-border ${responsive ? bp.overlayHidden : ''}`
352
+ ? `web:[grid-area:1/1] z-[120] h-full w-full rounded-[var(--bloom-panel-radius)] border border-border ${responsive ? bp.overlayHidden : ''}`
342
353
  : responsive
343
- ? `web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-radius-28 border border-border ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
344
- : 'web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-radius-28 border border-border web:[margin-bottom:calc(-100dvh+16px)]'
354
+ ? `web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] border border-border ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
355
+ : 'web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] border border-border web:[margin-bottom:calc(-100dvh+16px)]'
345
356
  }
346
357
  />
347
358
  )}
@@ -349,11 +360,11 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
349
360
  in place instead of remounting `{children}` (which would reset feed
350
361
  scroll/virtualizer + refetch on a breakpoint cross). Clipped to the
351
362
  rounded panel shape on web when framed. */}
352
- <StyledView key="content" testID="content-panel-content" className={contentClass} style={contentStyle}>
363
+ <StyledView key="content" testID="content-panel-content" className={contentClass} style={[curveStyle, framed === true ? { borderRadius: radius } : framed === false ? { borderRadius: 0 } : null, contentStyle]}>
353
364
  {/* The panel is a surface: everything inside is sitting on rung 1,
354
365
  painted in the colour this panel actually paints. */}
355
366
  <SurfaceLevelProvider level={1} fill={publishedFill}>
356
- {children}
367
+ {errorBoundary === false ? children : <PanelErrorBoundary {...(errorBoundary === true ? {} : errorBoundary)}>{children}</PanelErrorBoundary>}
357
368
  </SurfaceLevelProvider>
358
369
  </StyledView>
359
370
  </StyledView>
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { PanelErrorBoundaryOptions } from '../error-boundary/types';
2
3
  import { type StyleProp, type ViewStyle } from 'react-native';
3
4
 
4
5
  /**
@@ -17,8 +18,10 @@ export type ContentPanelChrome = 'elevated' | 'border' | 'none';
17
18
 
18
19
  export interface ContentPanelProps {
19
20
  children: React.ReactNode;
21
+ /** Opt in to local themed error recovery. Split panes already provide their own boundaries. */
22
+ errorBoundary?: boolean | PanelErrorBoundaryOptions;
20
23
  /**
21
- * Framing mode. Tri-state, resolved purely with NativeWind — no consumer
24
+ * Framing mode. Tri-state, managed by Bloom — no consumer
22
25
  * breakpoint hook needed:
23
26
  * - `undefined` (DEFAULT) → responsive: full-bleed below the `framedFrom`
24
27
  * breakpoint, rounded + bordered at/above it.
@@ -41,7 +44,7 @@ export interface ContentPanelProps {
41
44
  * surface lifted off the page rather than a box drawn on it.
42
45
  * - `'border'` — the hairline alone, which is what this panel drew before.
43
46
  * - `'none'` — no edge at all: a flat surface that still clips and rounds,
44
- * for an app whose page background already separates the two.
47
+ * without material rim or sheen, for an app whose page background separates the two.
45
48
  *
46
49
  * Ignored while the panel is full-bleed (below `framedFrom`, or
47
50
  * `framed={false}`) — there is no edge to draw.
@@ -4,7 +4,7 @@ import { RADIUS } from './scales';
4
4
  /** Component policies. Geometry and renderers never import these tokens. */
5
5
  export const SURFACE_SHAPES = {
6
6
  card: { radius: RADIUS['radius-12'], curve: 'smooth' },
7
- sidebar: { radius: RADIUS['radius-24'], curve: 'smooth' },
7
+ sidebar: { radius: RADIUS['radius-28'], curve: 'round' },
8
8
  panel: { radius: RADIUS['radius-20'], curve: 'smooth' },
9
9
  menu: { radius: RADIUS['radius-16'], curve: 'smooth' },
10
10
  sheet: { radius: { topStart: RADIUS['radius-24'], topEnd: RADIUS['radius-24'] }, curve: 'smooth' },
@@ -28596,12 +28596,12 @@
28596
28596
  "sidebar": {
28597
28597
  "$extensions": {
28598
28598
  "so.oxy.bloom": {
28599
- "curve": "smooth"
28599
+ "curve": "round"
28600
28600
  }
28601
28601
  },
28602
28602
  "radius": {
28603
28603
  "$type": "dimension",
28604
- "$value": "24px"
28604
+ "$value": "28px"
28605
28605
  }
28606
28606
  },
28607
28607
  "panel": {
@@ -83,6 +83,12 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
83
83
  this.props.onError?.(error, errorInfo);
84
84
  }
85
85
 
86
+ componentDidUpdate(previous: ErrorBoundaryProps): void {
87
+ if (this.state.hasError && !Object.is(previous.resetKey, this.props.resetKey)) {
88
+ this.setState({ hasError: false, error: null, errorInfo: null, retryCount: 0 });
89
+ }
90
+ }
91
+
86
92
  private handleRetry = (): void => {
87
93
  this.setState((prev) => ({
88
94
  hasError: false,
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { ScrollView } from 'react-native';
3
+ import { EmptyState } from '../empty-state';
4
+ import { useBottomEdgeInset } from '../layout/bottom-edge';
5
+ import { useMessages } from '../locale/messages';
6
+ import { space } from '../styles/tokens';
7
+ import { ErrorBoundary } from './ErrorBoundary';
8
+ import { ERROR_BOUNDARY_MESSAGES } from './messages';
9
+ import type { PanelErrorBoundaryProps } from './types';
10
+
11
+ /** Local recovery inside a healthy Bloom shell. Catastrophic/provider errors belong to ErrorBoundary. */
12
+ export function PanelErrorBoundary({ children, fallback, emptyState, bottomInset, ...props }: PanelErrorBoundaryProps) {
13
+ const { messages } = useMessages(ERROR_BOUNDARY_MESSAGES);
14
+ const inheritedBottom = useBottomEdgeInset();
15
+ return (
16
+ <ErrorBoundary {...props} fallback={fallback !== undefined ? fallback : (({ retry }) => (
17
+ <ScrollView
18
+ style={{ flex: 1, minHeight: 0 }}
19
+ contentContainerStyle={{ flexGrow: 1, paddingHorizontal: space.md, paddingBottom: bottomInset ?? inheritedBottom }}
20
+ >
21
+ <EmptyState
22
+ variant="compact"
23
+ {...emptyState}
24
+ title={emptyState?.title ?? messages.title}
25
+ description={emptyState?.description ?? messages.message}
26
+ style={[{ flexGrow: 1 }, emptyState?.style]}
27
+ action={{
28
+ ...emptyState?.action,
29
+ label: emptyState?.action?.label ?? messages.retry,
30
+ onPress: () => { emptyState?.action?.onPress?.(); retry(); },
31
+ }}
32
+ />
33
+ </ScrollView>
34
+ ))}>
35
+ {children}
36
+ </ErrorBoundary>
37
+ );
38
+ }
@@ -1,6 +1,9 @@
1
1
  export { ErrorBoundary } from './ErrorBoundary';
2
+ export { PanelErrorBoundary } from './PanelErrorBoundary';
2
3
  export type {
3
4
  ErrorBoundaryProps,
4
5
  ErrorBoundaryFallback,
5
6
  ErrorBoundaryFallbackContext,
7
+ PanelErrorBoundaryOptions,
8
+ PanelErrorBoundaryProps,
6
9
  } from './types';
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode, ErrorInfo } from 'react';
2
+ import type { EmptyStateProps } from '../empty-state/types';
2
3
 
3
4
  /**
4
5
  * Context passed to a render-prop `fallback` when an ErrorBoundary catches an
@@ -38,6 +39,8 @@ export type ErrorBoundaryFallback =
38
39
 
39
40
  export interface ErrorBoundaryProps {
40
41
  children: ReactNode;
42
+ /** Changing this identity resets a failed boundary without remounting healthy children. */
43
+ resetKey?: string | number;
41
44
  /**
42
45
  * Custom fallback UI to render on error. Accepts either:
43
46
  * - a ReactNode (static — same UI on every error), or
@@ -57,3 +60,18 @@ export interface ErrorBoundaryProps {
57
60
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
58
61
  testID?: string;
59
62
  }
63
+
64
+ /** Recovery UI for a panel inside a healthy Bloom provider. */
65
+ export interface PanelErrorBoundaryOptions {
66
+ resetKey?: ErrorBoundaryProps['resetKey'];
67
+ onError?: ErrorBoundaryProps['onError'];
68
+ fallback?: ErrorBoundaryFallback;
69
+ /** Defaults to localized error wording and a retry action. A supplied action runs before retry. */
70
+ emptyState?: EmptyStateProps;
71
+ /** Override inherited bottom chrome clearance when the parent has already reserved it. */
72
+ bottomInset?: number;
73
+ }
74
+
75
+ export interface PanelErrorBoundaryProps extends PanelErrorBoundaryOptions {
76
+ children: ReactNode;
77
+ }
package/src/index.ts CHANGED
@@ -204,6 +204,7 @@ export * from './patient-info-card';
204
204
  export * from './sidebar';
205
205
  export * from './app-shell';
206
206
  export * from './page-header';
207
+ export * from './page-footer';
207
208
  export * from './notification-center';
208
209
  export * from './theme-toggle';
209
210
  export * from './settings-modal';
@@ -219,11 +220,13 @@ export * from './task-list';
219
220
  export * from './agent-chat';
220
221
  export * from './ai-chat';
221
222
  export * from './radio-indicator';
222
- export { ErrorBoundary } from './error-boundary';
223
+ export { ErrorBoundary, PanelErrorBoundary } from './error-boundary';
223
224
  export type {
224
225
  ErrorBoundaryProps,
225
226
  ErrorBoundaryFallback,
226
227
  ErrorBoundaryFallbackContext,
228
+ PanelErrorBoundaryOptions,
229
+ PanelErrorBoundaryProps,
227
230
  } from './error-boundary';
228
231
  export * as Shapes from './shapes';
229
232
  export * from './avatar';
package/src/index.web.ts CHANGED
@@ -209,6 +209,7 @@ export * from './patient-info-card';
209
209
  export * from './sidebar';
210
210
  export * from './app-shell/index.web';
211
211
  export * from './page-header';
212
+ export * from './page-footer';
212
213
  export * from './notification-center';
213
214
  export * from './theme-toggle';
214
215
  export * from './settings-modal';
@@ -224,11 +225,13 @@ export * from './task-list';
224
225
  export * from './agent-chat/index.web';
225
226
  export * from './ai-chat/index.web';
226
227
  export * from './radio-indicator';
227
- export { ErrorBoundary } from './error-boundary';
228
+ export { ErrorBoundary, PanelErrorBoundary } from './error-boundary';
228
229
  export type {
229
230
  ErrorBoundaryProps,
230
231
  ErrorBoundaryFallback,
231
232
  ErrorBoundaryFallbackContext,
233
+ PanelErrorBoundaryOptions,
234
+ PanelErrorBoundaryProps,
232
235
  } from './error-boundary';
233
236
  export * as Shapes from './shapes';
234
237
  export * from './avatar';
@@ -131,22 +131,20 @@ export function MailMessage({
131
131
  : { key: 'forward', label: text.forward, icon: RiShareForwardLine, onPress: onForward },
132
132
  ].filter((entry): entry is NonNullable<typeof entry> => entry !== null);
133
133
 
134
- // An OPEN message is a surface, and Bloom's surface is a `Card` — the card
135
- // fill, the hairline, the radius scale — not a grey wash painted by hand. A
136
- // COLLAPSED one paints nothing, so a thread reads as one list with a card
137
- // opened inside it rather than as a column of blocks.
134
+ // Opening changes the content, not its surface or host. Keeping the Card
135
+ // mounted also preserves keyboard focus on the disclosure while it expands.
138
136
  // Three labelled actions need about 320 of the message's own width; under
139
137
  // that the group keeps the glyphs and drops the words, which is what
140
138
  // `ButtonGroupItem` is built for — the accessible name is the label either
141
139
  // way, so nothing is lost by the labels going.
142
140
  const compactReplies = !replyLabelsFit(width);
143
- const Shell = open ? Card : View;
144
- const shellProps = open
145
- ? { variant: 'outlined' as const, radius: 'radius-16' as const, testID }
146
- : { testID };
147
141
  return (
148
- <Shell
149
- {...shellProps}
142
+ <Card
143
+ appearance="solid"
144
+ border="none"
145
+ elevation="s"
146
+ radius="radius-16"
147
+ testID={testID}
150
148
  style={[open ? { paddingBottom: geo.paddingVertical } : null, style]}
151
149
  >
152
150
  <View
@@ -316,6 +314,6 @@ export function MailMessage({
316
314
  ) : null}
317
315
  </View>
318
316
  ) : null}
319
- </Shell>
317
+ </Card>
320
318
  );
321
319
  }
@@ -8,6 +8,7 @@ import { MailLabelChips, MailStar } from '../mail-list/parts';
8
8
  import { mailStrings, resolveMailPaint, visibleLabels } from '../mail-list/shared';
9
9
  import { adoptStyleSheet } from '../styles/adopt-style-sheet';
10
10
  import { useSurfaceFill } from '../styles/surface-levels';
11
+ import { space } from '../styles/tokens';
11
12
 
12
13
 
13
14
  import { useTheme } from '../theme/use-theme';
@@ -55,6 +56,7 @@ export function MailThread({
55
56
  expandedIds,
56
57
  onExpandedIdsChange,
57
58
  collapseAfter = 4,
59
+ messageGap = space.md,
58
60
  quickReply,
59
61
  header,
60
62
  strings,
@@ -144,7 +146,7 @@ export function MailThread({
144
146
  </View>
145
147
  ) : null}
146
148
  <Divider />
147
- <View style={{ paddingTop: 4, paddingBottom: 4 }}>
149
+ <View testID={testID ? `${testID}-messages` : undefined} style={{ paddingTop: space.xs, paddingBottom: space.xs, gap: messageGap }}>
148
150
  {entries.map((entry) =>
149
151
  entry.kind === 'message' ? (
150
152
  <MailMessage
@@ -1,4 +1,5 @@
1
1
  import { Platform } from 'react-native';
2
+ import { RADIUS } from '../design-tokens/scales';
2
3
 
3
4
  import { MAIL_THREAD_MESSAGES } from './messages';
4
5
  import type { MailThreadMessage, MailAddress, MailThreadStrings } from './types';
@@ -44,7 +45,7 @@ export const MAIL_THREAD_GEOMETRY = {
44
45
  /** The star and the overflow trigger. A touch target, so 44. */
45
46
  action: 44,
46
47
  /** Radius of a message's own surface. */
47
- radius: 12,
48
+ radius: RADIUS['radius-16'],
48
49
  } as const;
49
50
 
50
51
  /**
@@ -128,6 +128,8 @@ export interface MailThreadProps {
128
128
  * Default 4; `0` never collapses.
129
129
  */
130
130
  collapseAfter?: number;
131
+ /** Space between message cards and the earlier-messages control. Default space.md (12). */
132
+ messageGap?: number;
131
133
  /** A slot under the stack — the quick-reply box, or a row of reply buttons. */
132
134
  quickReply?: ReactNode;
133
135
  /** A slot above the subject — a back button, the mailbox chrome. */