@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
@@ -0,0 +1,26 @@
1
+ import type { ContextType, ReactNode } from 'react';
2
+ import {
3
+ CharacterCapabilitiesContext,
4
+ CharacterRuntimeContext,
5
+ } from '../../../agent-avatar/context';
6
+
7
+ /** Supply both internal channels without starting the browser engine in UI tests. */
8
+ export function CharacterRuntimeFixture({
9
+ value,
10
+ children,
11
+ }: {
12
+ value: ContextType<typeof CharacterRuntimeContext> &
13
+ ContextType<typeof CharacterCapabilitiesContext>;
14
+ children: ReactNode;
15
+ }) {
16
+ const { runtimeUrl, reportCapabilities, ...catalog } = value;
17
+ return (
18
+ <CharacterRuntimeContext.Provider
19
+ value={{ runtimeUrl, reportCapabilities }}
20
+ >
21
+ <CharacterCapabilitiesContext.Provider value={catalog}>
22
+ {children}
23
+ </CharacterCapabilitiesContext.Provider>
24
+ </CharacterRuntimeContext.Provider>
25
+ );
26
+ }
@@ -84,7 +84,7 @@ export function createAppShell(BottomBar: ComponentType<BottomBarProps>, Fab: Co
84
84
  ) : null}
85
85
  <Screen navigationScope="shared" active={active} documentScroll={scroll === 'external'} testID={testID ? `${testID}-screen` : undefined} style={{ minWidth: 0 }} header={header ?? (title ? <PageHeader title={title} subtitle={breadcrumb} actions={actions} sticky={false} /> : null)} bottomBar={bottom} primaryAction={placement !== 'bottom' && primaryAction ? <Fab {...primaryAction} /> : undefined}>
86
86
  {scroll === 'auto' ? <ScreenScrollView style={{ flex: 1 }}>
87
- <View style={{ width: '100%', maxWidth: contentMaxWidth, alignSelf: 'center', padding: 16, gap: 16 }}>{children}</View>
87
+ <View testID={testID ? `${testID}-content` : undefined} style={{ width: '100%', maxWidth: contentMaxWidth === 'none' ? undefined : contentMaxWidth, alignSelf: 'center', padding: 16, gap: 16 }}>{children}</View>
88
88
  </ScreenScrollView> : children}
89
89
  {overlay}
90
90
  </Screen>
@@ -151,7 +151,7 @@ export function createAppShell(BottomBar: ComponentType<BottomBarProps>, Fab: Co
151
151
  || props.primaryAction !== undefined || props.scroll === 'auto' || props.scroll === 'external';
152
152
  if (!adaptive) return <AppShellEngine {...props as AppShellEngineProps} />;
153
153
  const scroll = props.scroll ?? (Platform.OS === 'web' ? 'document' : 'auto');
154
- const usesLayout = props.panel !== undefined || props.panelColorPreset !== undefined || props.variant !== undefined || props.aside !== undefined || props.list !== undefined || props.info !== undefined
154
+ const usesLayout = props.safeArea !== undefined || props.panel !== undefined || props.panelColorPreset !== undefined || props.variant !== undefined || props.aside !== undefined || props.list !== undefined || props.info !== undefined
155
155
  || props.contentWidth !== undefined || props.navigationAlign !== undefined || props.navigationGap !== undefined || props.drawer !== undefined
156
156
  || props.navFrom !== undefined || props.navExpandedFrom !== undefined || props.topBar !== undefined
157
157
  || props.bottomBar !== undefined || props.floatingAction !== undefined;
@@ -36,13 +36,13 @@ import { useClaimBottomEdge } from '../layout/bottom-edge';
36
36
  import { Z_INDEX } from '../styles/z-index';
37
37
 
38
38
  /** The safe-area insets, or zeros outside a `SafeAreaProvider`. */
39
- export function useShellInsets(): { top: number; bottom: number } {
39
+ export function useShellInsets(): { top: number; right: number; bottom: number; left: number } {
40
40
  // `useContext(SafeAreaInsetsContext)` rather than `useSafeAreaInsets()`: the
41
41
  // hook THROWS outside a provider, and `AppShell` is mounted at an app's root,
42
42
  // where making a previously-optional provider mandatory would be a breaking
43
43
  // change for every consumer that never had a bar to inset.
44
44
  const insets = useContext(SafeAreaInsetsContext);
45
- return { top: insets?.top ?? 0, bottom: insets?.bottom ?? 0 };
45
+ return insets ?? { top: 0, right: 0, bottom: 0, left: 0 };
46
46
  }
47
47
 
48
48
  /** Pinned to the bottom of the viewport (document scroll) or of the frame. */
@@ -152,6 +152,8 @@ export const AppShellFloatingAction = memo(AppShellFloatingActionComponent);
152
152
  AppShellFloatingAction.displayName = 'AppShellFloatingAction';
153
153
 
154
154
  export interface AppShellTopBarProps {
155
+ /** The enclosing safe frame may already own the top inset. */
156
+ insetTop?: number;
155
157
  children: React.ReactNode;
156
158
  onHeightChange?: (height: number) => void;
157
159
  doc: boolean;
@@ -167,11 +169,11 @@ export interface AppShellTopBarProps {
167
169
  * only as far as `top: 0`); in `container`/`fixed` and on native it is simply
168
170
  * outside the scroller, which pins it by construction.
169
171
  */
170
- const AppShellTopBarComponent: React.FC<AppShellTopBarProps> = ({ children, doc, testID, onHeightChange }) => {
172
+ const AppShellTopBarComponent: React.FC<AppShellTopBarProps> = ({ children, doc, testID, onHeightChange, insetTop }) => {
171
173
  const insets = useShellInsets();
172
174
  const pinned: WebCssStyle = doc ? { position: WEB_POSITION_STICKY, top: 0 } : {};
173
175
  return (
174
- <View testID={testID} onLayout={event => onHeightChange?.(event.nativeEvent.layout.height)} style={[{ zIndex: Z_INDEX.floating, paddingTop: insets.top }, pinned]}>
176
+ <View testID={testID} onLayout={event => onHeightChange?.(event.nativeEvent.layout.height)} style={[{ zIndex: Z_INDEX.floating, paddingTop: insetTop ?? insets.top }, pinned]}>
175
177
  {children}
176
178
  </View>
177
179
  );
@@ -106,6 +106,7 @@ export function createAppShellEngine(
106
106
  ) {
107
107
  const AppShellComponent: React.FC<AppShellEngineProps> = ({
108
108
  variant = 'dashboard',
109
+ safeArea = false,
109
110
  navigationAlign = 'edge',
110
111
  navigationGap,
111
112
  asideGap,
@@ -166,6 +167,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
166
167
  const [asideHeight, setAsideHeight] = useState(0);
167
168
  const { width, onLayout } = useShellWidth();
168
169
  const insets = useShellInsets();
170
+ const ownsSafeArea = safeArea && Platform.OS !== 'web';
169
171
  const mode = resolveScrollMode(variant, scroll);
170
172
  const doc = mode === 'document';
171
173
  const fixed = mode === 'fixed';
@@ -175,6 +177,10 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
175
177
  // for every other shape.
176
178
  const gutter = gutterProp ?? (variant === 'dashboard' ? D.dashboardGutter : D.gutter);
177
179
  const columnGap = variant === 'dashboard' ? D.dashboardColumnGap : gutter;
180
+ // Historical reveal geometry stays the default; an explicit gutter owns
181
+ // every frame edge, including the old extra top gap at wider breakpoints.
182
+ const revealGutter = gutterProp ?? 12;
183
+ const revealTopGutter = gutterProp ?? (small ? 24 : 12);
178
184
 
179
185
  // The nav: `navExpandedFrom` decides which sidebar variant is drawn, and the
180
186
  // variant decides how early it can sit in flow (a rail is narrow enough at
@@ -290,6 +296,24 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
290
296
  const showTopBar = topBar != null && barActive(topBarVisibility);
291
297
  const showBottomBar = bottomBar != null && barActive(bottomBarVisibility);
292
298
  const showFloatingAction = floatingAction != null;
299
+ // Preserve the real device insets for fullscreen dialogs and portals. Only
300
+ // the bounded shell consumes these edges; bottom chrome keeps its own inset.
301
+ const bottomChrome = showBottomBar || showFloatingAction;
302
+ const frameBottomInset = ownsSafeArea && !bottomChrome ? insets.bottom : 0;
303
+ const navigationBottomInset = ownsSafeArea && bottomChrome ? insets.bottom : 0;
304
+ const safeFrame = (node: React.ReactElement) => ownsSafeArea ? (
305
+ <View {...dirProps} testID={testID ? `${testID}-safe-area` : undefined} style={{
306
+ flex: 1, minHeight: 0, minWidth: 0,
307
+ paddingTop: insets.top,
308
+ paddingInlineStart: rtl ? insets.right : insets.left,
309
+ paddingInlineEnd: rtl ? insets.left : insets.right,
310
+ paddingBottom: frameBottomInset, backgroundColor: background,
311
+ }}>{node}</View>
312
+ ) : node;
313
+
314
+ const safeNavigation = (node: React.ReactElement) => navigationBottomInset ? (
315
+ <View style={{ flexShrink: 0, paddingBottom: navigationBottomInset }}>{node}</View>
316
+ ) : node;
293
317
  const [topBarHeight, setTopBarHeight] = useState(0);
294
318
  const [bottomBarHeight, setBottomBarHeight] = useState(0);
295
319
  const [floatingActionHeight, setFloatingActionHeight] = useState(0);
@@ -328,7 +352,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
328
352
  );
329
353
 
330
354
  const topBarNode = showTopBar ? (
331
- <AppShellTopBar doc={doc} onHeightChange={setTopBarHeight} testID={testID ? `${testID}-top-bar` : undefined}>
355
+ <AppShellTopBar insetTop={ownsSafeArea ? 0 : undefined} doc={doc} onHeightChange={setTopBarHeight} testID={testID ? `${testID}-top-bar` : undefined}>
332
356
  {topBar}
333
357
  </AppShellTopBar>
334
358
  ) : null;
@@ -363,7 +387,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
363
387
 
364
388
  const fill = fixed ? { flex: 1, minHeight: 0 } : null;
365
389
  const column = (
366
- <View style={[{ width: '100%', maxWidth: contentMaxWidth, alignSelf: 'center', gap: 10 }, fill]}>
390
+ <View testID={testID ? `${testID}-content` : undefined} style={[{ width: '100%', maxWidth: contentMaxWidth === 'none' ? undefined : contentMaxWidth, alignSelf: 'center', gap: 10 }, fill]}>
367
391
  {headerNode}
368
392
  <View
369
393
  style={[
@@ -432,7 +456,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
432
456
  const veilStyle = useAnimatedStyle(() => ({ opacity: reveal.value }), [reveal]);
433
457
 
434
458
  if (drawerStyle === 'reveal' && variant === 'dashboard') {
435
- return (
459
+ return safeFrame(
436
460
  <AppShellProvider value={shell}>
437
461
  <View
438
462
  {...dirProps}
@@ -445,20 +469,21 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
445
469
  style={[
446
470
  flowSidebar.surface === 'docked'
447
471
  ? { flexShrink: 0 }
448
- : { paddingTop: 12, paddingBottom: 12, paddingInlineStart: 12, flexShrink: 0 },
472
+ : { paddingTop: revealGutter, paddingBottom: revealGutter, paddingInlineStart: revealGutter, flexShrink: 0 },
449
473
  doc ? stickyRail(0) : null,
450
474
  ]}
451
475
  >
452
- <Sidebar {...flowSidebar} />
476
+ {safeNavigation(<Sidebar {...flowSidebar} />)}
453
477
  </View>
454
478
  ) : null}
455
479
  {drawerAvailable && drawerSidebar ? (
456
480
  <View
481
+ testID={testID ? `${testID}-navigation-drawer` : undefined}
457
482
  aria-hidden={!isOpen}
458
483
  pointerEvents={isOpen ? 'auto' : 'none'}
459
484
  // Fixed on web in document mode: the rail waits beneath the VIEWPORT,
460
485
  // wherever the page is scrolled to.
461
- style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingInlineStart: 6 }}
486
+ style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: revealGutter, paddingBottom: revealGutter + navigationBottomInset, paddingInlineStart: revealGutter / 2 }}
462
487
  >
463
488
  <Animated.View style={[{ height: '100%', width: 260, transformOrigin: railOrigin }, railStyle]}>
464
489
  <Sidebar {...drawerSidebar} mobile surface="plain" onClose={() => setOpen(false)} />
@@ -487,13 +512,13 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
487
512
  // layered over the shorthand, and only when there is one.
488
513
  contentStyle={
489
514
  contentReserve
490
- ? { paddingLeft: 12, paddingRight: 12, paddingTop: small ? 24 : 12, paddingBottom: 12 + contentReserve }
491
- : { padding: 12, paddingTop: small ? 24 : 12 }
515
+ ? { paddingLeft: revealGutter, paddingRight: revealGutter, paddingTop: revealTopGutter, paddingBottom: revealGutter + contentReserve }
516
+ : { padding: revealGutter, paddingTop: revealTopGutter }
492
517
  }
493
518
  >
494
519
  {column}
495
520
  </Scroller>
496
- {asideColumn({ paddingTop: small ? 24 : 12, paddingBottom: 12, paddingInlineEnd: 12 })}
521
+ {asideColumn({ paddingTop: revealTopGutter, paddingBottom: revealGutter, paddingInlineEnd: revealGutter })}
497
522
  {!wide ? (
498
523
  <Animated.View
499
524
  pointerEvents={isOpen ? 'auto' : 'none'}
@@ -758,15 +783,15 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
758
783
  navInFlow && flowSidebar ? (
759
784
  doc ? (
760
785
  <View testID={testID ? `${testID}-navigation` : undefined} style={stickyRail(dockedNav || plainFrame ? 0 : gutter)}>
761
- <Sidebar {...flowSidebar} />
786
+ {safeNavigation(<Sidebar {...flowSidebar} />)}
762
787
  </View>
763
788
  ) : canvas && !dockedNav ? (
764
789
  // The canvas row has no padding of its own, so the nav states its.
765
790
  <View style={{ paddingTop: gutter, paddingBottom: gutter, paddingInlineStart: gutter, flexShrink: 0 }}>
766
- <Sidebar {...flowSidebar} />
791
+ {safeNavigation(<Sidebar {...flowSidebar} />)}
767
792
  </View>
768
793
  ) : (
769
- <Sidebar {...flowSidebar} />
794
+ safeNavigation(<Sidebar {...flowSidebar} />)
770
795
  )
771
796
  ) : null;
772
797
 
@@ -783,7 +808,15 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
783
808
  <View
784
809
  {...dirProps}
785
810
  pointerEvents="box-none"
786
- style={{ position: 'absolute', top: 0, bottom: 0, insetInlineStart: 0, flexDirection: 'row', padding: 12 }}
811
+ testID={testID ? `${testID}-navigation-drawer` : undefined}
812
+ style={{ position: 'absolute', top: 0, bottom: 0, insetInlineStart: 0, flexDirection: 'row',
813
+ ...(ownsSafeArea ? {
814
+ paddingTop: 12 + insets.top,
815
+ paddingBottom: 12 + insets.bottom,
816
+ paddingInlineStart: 12 + (rtl ? insets.right : insets.left),
817
+ paddingInlineEnd: 12 + (rtl ? insets.left : insets.right),
818
+ } : { padding: 12 }),
819
+ }}
787
820
  >
788
821
  <View pointerEvents="auto" style={{ height: '100%' }}>
789
822
  <Sidebar {...drawerSidebar} mobile onClose={() => setOpen(false)} />
@@ -810,12 +843,12 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
810
843
  };
811
844
 
812
845
  if (variant === 'feed' && drawerStyle === 'reveal' && drawerAvailable) {
813
- return (
846
+ return safeFrame(
814
847
  <AppShellProvider value={shell}>
815
848
  <View {...dirProps} testID={testID} onLayout={onLayout}
816
849
  style={[frame, { overflow: doc ? WEB_OVERFLOW_CLIP : 'hidden', backgroundColor: background }, style]}>
817
- <View aria-hidden={!isOpen} pointerEvents={isOpen ? 'auto' : 'none'}
818
- style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingInlineStart: 6 }}>
850
+ <View testID={testID ? `${testID}-navigation-drawer` : undefined} aria-hidden={!isOpen} pointerEvents={isOpen ? 'auto' : 'none'}
851
+ style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: revealGutter, paddingBottom: revealGutter + navigationBottomInset, paddingInlineStart: revealGutter / 2 }}>
819
852
  <Animated.View style={[{ height: '100%', width: 260, transformOrigin: railOrigin }, railStyle]}>
820
853
  {drawerSidebar && <Sidebar {...drawerSidebar} mobile surface="plain" onClose={() => setOpen(false)} />}
821
854
  </Animated.View>
@@ -850,7 +883,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
850
883
  // edge without escaping the frame's padding. Without one the root stays the
851
884
  // row it has always been, so nothing about an existing shell moves.
852
885
  if (topBarNode) {
853
- return (
886
+ return safeFrame(
854
887
  <AppShellProvider value={shell}>
855
888
  <View
856
889
  {...dirProps}
@@ -871,7 +904,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
871
904
  );
872
905
  }
873
906
 
874
- return (
907
+ return safeFrame(
875
908
  <AppShellProvider value={shell}>
876
909
  <View {...dirProps} testID={testID} onLayout={onLayout} style={[frame, rowStyle, style]}>
877
910
  {navRegion}
@@ -18,6 +18,8 @@ import React, { memo, useCallback, useRef, useState } from 'react';
18
18
  import { ScrollView, View } from 'react-native';
19
19
 
20
20
  import { AiChatResizeHandle } from '../ai-chat/AiChatResizeHandle';
21
+ import { PanelErrorBoundary } from '../error-boundary/PanelErrorBoundary';
22
+ import type { PanelErrorBoundaryOptions } from '../error-boundary/types';
21
23
  import { Z_INDEX } from '../styles/z-index';
22
24
  import { space } from '../styles/tokens';
23
25
  import { useTheme } from '../theme/use-theme';
@@ -31,29 +33,33 @@ import { APP_SHELL_DEFAULTS as D } from './constants';
31
33
  function Pane({
32
34
  children,
33
35
  scroll,
36
+ errorBoundary,
34
37
  style,
35
38
  testID,
36
39
  }: {
37
40
  children: React.ReactNode;
38
41
  scroll: boolean;
42
+ errorBoundary?: false | PanelErrorBoundaryOptions;
39
43
  style: React.ComponentProps<typeof View>['style'];
40
44
  testID?: string;
41
45
  }) {
46
+ const content = scroll ? (
47
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1 }}>
48
+ {children}
49
+ </ScrollView>
50
+ ) : <View style={{ flex: 1, minHeight: 0 }}>{children}</View>;
42
51
  return (
43
52
  <View testID={testID} style={[{ minWidth: 0, alignSelf: 'stretch' }, style]}>
44
- {scroll ? (
45
- <ScrollView style={{ flex: 1 }} contentContainerStyle={{ flexGrow: 1 }}>
46
- {children}
47
- </ScrollView>
48
- ) : (
49
- <View style={{ flex: 1, minHeight: 0 }}>{children}</View>
50
- )}
53
+ {errorBoundary === false ? content : <PanelErrorBoundary {...errorBoundary}>{content}</PanelErrorBoundary>}
51
54
  </View>
52
55
  );
53
56
  }
54
57
 
55
58
  const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
56
59
  variant = 'joined',
60
+ listErrorBoundary,
61
+ detailErrorBoundary,
62
+ infoErrorBoundary,
57
63
  list,
58
64
  detail,
59
65
  info,
@@ -120,6 +126,7 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
120
126
  {showList ? (
121
127
  <Pane
122
128
  testID={testID ? `${testID}-pane-list` : undefined}
129
+ errorBoundary={listErrorBoundary}
123
130
  scroll={paneScroll}
124
131
  style={solo ? { flexGrow: 1, flexShrink: 1, flexBasis: 0 } : { width, flexShrink: 0 }}
125
132
  >
@@ -148,6 +155,7 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
148
155
  {showDetail ? (
149
156
  <Pane
150
157
  testID={testID ? `${testID}-pane-detail` : undefined}
158
+ errorBoundary={detailErrorBoundary}
151
159
  scroll={paneScroll}
152
160
  style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }}
153
161
  >
@@ -158,6 +166,7 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
158
166
  {showInfo ? (
159
167
  <Pane
160
168
  testID={testID ? `${testID}-pane-info` : undefined}
169
+ errorBoundary={infoErrorBoundary}
161
170
  scroll={paneScroll}
162
171
  style={solo ? { flexGrow: 1, flexShrink: 1, flexBasis: 0 } : { width: infoWidth, flexShrink: 0 }}
163
172
  >
@@ -2,6 +2,7 @@ import type { AppColorName } from '../theme/color-presets';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { StyleProp, ViewStyle } from 'react-native';
4
4
 
5
+ import type { PanelErrorBoundaryOptions } from '../error-boundary/types';
5
6
  import type { ContentPanelFramedBreakpoint } from '../content-panel/types';
6
7
  import type { NotificationCenterItem, NotificationCenterTab } from '../notification-center/types';
7
8
  import type { SidebarProps } from '../sidebar/types';
@@ -67,6 +68,14 @@ export type AppShellScroll = 'document' | 'container' | 'fixed';
67
68
  export type AppShellBarVisibility = 'compact' | 'always';
68
69
 
69
70
  export interface AppShellEngineProps {
71
+ /**
72
+ * Native: keep the shell, navigation and pages inside the device's safe area.
73
+ * The bottom-bar slot still owns its bottom inset; without bottom chrome the
74
+ * frame reserves it. Use `safeArea={false}` on descendant PageHeaders because
75
+ * the frame already owns their top edge. Device insets remain available to
76
+ * dialogs and other portaled surfaces. Defaults to false; no effect on web.
77
+ */
78
+ safeArea?: boolean;
70
79
  /**
71
80
  * The layout shape. Defaults to `dashboard` — the behaviour this component
72
81
  * had before variants existed, down to the pixel.
@@ -124,8 +133,8 @@ export interface AppShellEngineProps {
124
133
  */
125
134
  asideCollapse?: 'stack' | 'hidden';
126
135
  children?: ReactNode;
127
- /** The content column's max width. Defaults to 1300. */
128
- contentMaxWidth?: number;
136
+ /** The content column's max width. Defaults to 1300; `none` fills the available width. */
137
+ contentMaxWidth?: number | 'none';
129
138
  /** Floating extras rendered last, e.g. a `ProOfferCard`. */
130
139
  overlay?: ReactNode;
131
140
  /** Controlled drawer state (below `lg`). */
@@ -364,7 +373,7 @@ export interface AppShellNavigationProps {
364
373
  actions?: ReactNode;
365
374
  header?: ReactNode;
366
375
  children?: ReactNode;
367
- contentMaxWidth?: number;
376
+ contentMaxWidth?: number | 'none';
368
377
  overlay?: ReactNode;
369
378
  style?: StyleProp<ViewStyle>;
370
379
  testID?: string;
@@ -379,6 +388,10 @@ export type AppShellProps = Omit<AppShellEngineProps, 'scroll'> & AppShellNaviga
379
388
 
380
389
  /** A bounded list/detail/info layout, independent of navigation or surfaces. */
381
390
  export interface AppShellSplitPanesProps {
391
+ /** Each visible pane isolates render errors by default. false delegates to an outer boundary. */
392
+ listErrorBoundary?: false | PanelErrorBoundaryOptions;
393
+ detailErrorBoundary?: false | PanelErrorBoundaryOptions;
394
+ infoErrorBoundary?: false | PanelErrorBoundaryOptions;
382
395
  /** Joined uses hairline dividers (default); separated leaves a 12px gutter for independent panel surfaces. */
383
396
  variant?: 'joined' | 'separated';
384
397
  list?: ReactNode;
@@ -3,8 +3,11 @@ import { BloomAppearanceContext } from './context';
3
3
  import type { BloomScopeProps } from './types';
4
4
 
5
5
  /** A visual environment only: never inherits actions, selection or disabled state. */
6
- export function BloomScope({ children, size, tone }: BloomScopeProps) {
6
+ export function BloomScope({ children, size, tone, panelRadius }: BloomScopeProps) {
7
+ if (panelRadius !== undefined && (!Number.isFinite(panelRadius) || panelRadius < 0)) {
8
+ throw new RangeError('panelRadius must be a finite, non-negative number.');
9
+ }
7
10
  const parent = useContext(BloomAppearanceContext);
8
- const value = useMemo(() => ({ size: size ?? parent.size, tone: tone ?? parent.tone }), [size, tone, parent]);
11
+ const value = useMemo(() => ({ size: size ?? parent.size, tone: tone ?? parent.tone, panelRadius: panelRadius ?? parent.panelRadius }), [size, tone, panelRadius, parent]);
9
12
  return <BloomAppearanceContext.Provider value={value}>{children}</BloomAppearanceContext.Provider>;
10
13
  }
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from 'react';
2
- import type { BloomAppearanceProps } from './types';
2
+ import type { BloomAppearanceProps, BloomScopeProps } from './types';
3
3
 
4
- export const BloomAppearanceContext = createContext<BloomAppearanceProps>({});
4
+ export const BloomAppearanceContext = createContext<Omit<BloomScopeProps, 'children'>>({});
5
5
 
6
6
  /** Explicit props override the nearest scope, then the family's defaults. */
7
7
  export function useBloomAppearance(
@@ -8,4 +8,7 @@ export interface BloomAppearanceProps {
8
8
  size?: BloomSize;
9
9
  tone?: BloomTone;
10
10
  }
11
- export type BloomScopeProps = PropsWithChildren<BloomAppearanceProps>;
11
+ export type BloomScopeProps = PropsWithChildren<BloomAppearanceProps & {
12
+ /** Shared corner radius for framed content panels and card sidebars. Defaults to 28. */
13
+ panelRadius?: number;
14
+ }>;
package/src/chip/Chip.tsx CHANGED
@@ -84,6 +84,15 @@ const BLOOM_CHIP_CSS = interactiveWebCss({
84
84
  hover: { declarations: 'opacity: 0.9;' },
85
85
  outlineOffset: 2,
86
86
  extraRules: `
87
+ /* The removable pill contains two sibling controls. Draw one exterior focus ring. */
88
+ [data-bloom-chip-group]:has(:focus-visible) {
89
+ outline: 2px solid var(--bloom-chip-ring);
90
+ outline-offset: 2px;
91
+ }
92
+ [data-bloom-chip-group] [data-bloom-chip]:focus-visible {
93
+ outline: none;
94
+ }
95
+
87
96
  /* The rung with a minWidth ("Any", "1", "8+" in a counts row) centres its
88
97
  label. The style prop cannot say so: the reset above is an adopted sheet and
89
98
  wins over react-native-web's atomic classes, so it has to be said here too. */
@@ -214,7 +223,9 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
214
223
  const removeLabel = closeLabel ?? (typeof children === 'string' ? `Remove ${children}` : 'Remove');
215
224
  const closeButton = onClose ? (
216
225
  <Pressable
217
- onPress={onClose}
226
+ onPress={(event) => { event.stopPropagation(); onClose(); }}
227
+ disabled={disabled}
228
+ aria-disabled={disabled || undefined}
218
229
  hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
219
230
  accessibilityLabel={removeLabel}
220
231
  accessibilityRole="button"
@@ -235,7 +246,6 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
235
246
  children
236
247
  )}
237
248
  {endIcon != null ? <View style={iconSlotStyle}>{endIcon}</View> : null}
238
- {closeButton}
239
249
  </>
240
250
  );
241
251
 
@@ -256,9 +266,9 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
256
266
  ? { accessibilityState: { selected, disabled }, 'aria-selected': selected }
257
267
  : { accessibilityState: { disabled, selected }, 'aria-pressed': selected };
258
268
 
259
- return (
269
+ const button = (
260
270
  <StyledPressable
261
- className={className}
271
+ className={onClose ? undefined : className}
262
272
  ref={ref}
263
273
  // The DOM hook the adopted sheet above hangs off.
264
274
  //
@@ -286,7 +296,7 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
286
296
  ...(onKeyDown ? { onKeyDown } : null),
287
297
  } as Record<string, unknown>)
288
298
  : {})}
289
- style={[
299
+ style={onClose ? { flexGrow: 1, flexShrink: 1, alignSelf: 'stretch', flexDirection: 'row', alignItems: 'center', gap: geometry.iconGap, borderWidth: 0, backgroundColor: 'transparent', padding: 0, minWidth: 0 } : [
290
300
  containerStyle,
291
301
  disabled && { opacity: 0.5 },
292
302
  hovered && !disabled && paint.hoveredBorder ? { borderColor: paint.hoveredBorder } : null,
@@ -312,6 +322,16 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
312
322
  {content}
313
323
  </StyledPressable>
314
324
  );
325
+ if (!onClose) return button;
326
+ // Selection and removal are sibling controls, never nested buttons.
327
+ // The shared outer pill owns layout classes and paint; the ref still targets selection.
328
+ return <StyledView
329
+ className={className}
330
+ {...(IS_WEB ? { dataSet: { bloomChipGroup: '' } } : {})}
331
+ style={[containerStyle, disabled && { opacity: 0.5 },
332
+ hovered && !disabled && paint.hoveredBorder ? { borderColor: paint.hoveredBorder } : null,
333
+ pressed && !disabled && { backgroundColor: paint.pressedBackground }, style]}
334
+ >{button}{closeButton}</StyledView>;
315
335
  }
316
336
 
317
337
  return (
@@ -323,6 +343,7 @@ const ChipComponent = forwardRef<View, ChipProps>(function ChipComponent(
323
343
  testID={testID}
324
344
  >
325
345
  {content}
346
+ {closeButton}
326
347
  </StyledView>
327
348
  );
328
349
  });