@oxy.so/bloom 7.1.5 → 7.2.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 (223) hide show
  1. package/docs/adoption-matrix.mdx +12 -11
  2. package/docs/app-shell.mdx +26 -2
  3. package/docs/appearance.mdx +16 -1
  4. package/docs/content-panel.mdx +4 -0
  5. package/docs/mail-thread.mdx +8 -0
  6. package/docs/page-footer.mdx +60 -0
  7. package/docs/sidebar.mdx +1 -1
  8. package/lib/commonjs/app-shell/AppShell.js +3 -2
  9. package/lib/commonjs/app-shell/AppShell.js.map +1 -1
  10. package/lib/commonjs/app-shell/AppShellBars.js +8 -5
  11. package/lib/commonjs/app-shell/AppShellBars.js.map +1 -1
  12. package/lib/commonjs/app-shell/AppShellEngine.js +81 -36
  13. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  14. package/lib/commonjs/appearance/BloomScope.js +8 -3
  15. package/lib/commonjs/appearance/BloomScope.js.map +1 -1
  16. package/lib/commonjs/appearance/context.js.map +1 -1
  17. package/lib/commonjs/content-panel/ContentPanel.js +34 -23
  18. package/lib/commonjs/content-panel/ContentPanel.js.map +1 -1
  19. package/lib/commonjs/content-panel/ContentPanel.web.js +39 -17
  20. package/lib/commonjs/content-panel/ContentPanel.web.js.map +1 -1
  21. package/lib/commonjs/design-tokens/shapes.js +2 -2
  22. package/lib/commonjs/design-tokens/shapes.js.map +1 -1
  23. package/lib/commonjs/design-tokens/tokens.json +2 -2
  24. package/lib/commonjs/index.js +939 -927
  25. package/lib/commonjs/index.js.map +1 -1
  26. package/lib/commonjs/index.web.js +939 -927
  27. package/lib/commonjs/index.web.js.map +1 -1
  28. package/lib/commonjs/mail-thread/MailMessage.js +8 -14
  29. package/lib/commonjs/mail-thread/MailMessage.js.map +1 -1
  30. package/lib/commonjs/mail-thread/MailThread.js +6 -2
  31. package/lib/commonjs/mail-thread/MailThread.js.map +1 -1
  32. package/lib/commonjs/mail-thread/shared.js +2 -1
  33. package/lib/commonjs/mail-thread/shared.js.map +1 -1
  34. package/lib/commonjs/page-footer/PageFooter.js +137 -0
  35. package/lib/commonjs/page-footer/PageFooter.js.map +1 -0
  36. package/lib/commonjs/page-footer/PageFooterProvider.js +22 -0
  37. package/lib/commonjs/page-footer/PageFooterProvider.js.map +1 -0
  38. package/lib/commonjs/page-footer/context.js +11 -0
  39. package/lib/commonjs/page-footer/context.js.map +1 -0
  40. package/lib/commonjs/page-footer/index.js +27 -0
  41. package/lib/commonjs/page-footer/index.js.map +1 -0
  42. package/lib/commonjs/page-footer/types.js +6 -0
  43. package/lib/commonjs/page-footer/types.js.map +1 -0
  44. package/lib/commonjs/page-footer/use-page-footer-inset.js +19 -0
  45. package/lib/commonjs/page-footer/use-page-footer-inset.js.map +1 -0
  46. package/lib/commonjs/sidebar/Sidebar.js +6 -5
  47. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  48. package/lib/commonjs/styles/panel-chrome.js +15 -1
  49. package/lib/commonjs/styles/panel-chrome.js.map +1 -1
  50. package/lib/module/app-shell/AppShell.js +3 -2
  51. package/lib/module/app-shell/AppShell.js.map +1 -1
  52. package/lib/module/app-shell/AppShellBars.js +8 -5
  53. package/lib/module/app-shell/AppShellBars.js.map +1 -1
  54. package/lib/module/app-shell/AppShellEngine.js +81 -36
  55. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  56. package/lib/module/appearance/BloomScope.js +8 -3
  57. package/lib/module/appearance/BloomScope.js.map +1 -1
  58. package/lib/module/appearance/context.js.map +1 -1
  59. package/lib/module/content-panel/ContentPanel.js +35 -24
  60. package/lib/module/content-panel/ContentPanel.js.map +1 -1
  61. package/lib/module/content-panel/ContentPanel.web.js +41 -19
  62. package/lib/module/content-panel/ContentPanel.web.js.map +1 -1
  63. package/lib/module/design-tokens/shapes.js +2 -2
  64. package/lib/module/design-tokens/shapes.js.map +1 -1
  65. package/lib/module/design-tokens/tokens.json +2 -2
  66. package/lib/module/index.js +1 -0
  67. package/lib/module/index.js.map +1 -1
  68. package/lib/module/index.web.js +1 -0
  69. package/lib/module/index.web.js.map +1 -1
  70. package/lib/module/mail-thread/MailMessage.js +8 -14
  71. package/lib/module/mail-thread/MailMessage.js.map +1 -1
  72. package/lib/module/mail-thread/MailThread.js +6 -2
  73. package/lib/module/mail-thread/MailThread.js.map +1 -1
  74. package/lib/module/mail-thread/shared.js +2 -1
  75. package/lib/module/mail-thread/shared.js.map +1 -1
  76. package/lib/module/page-footer/PageFooter.js +132 -0
  77. package/lib/module/page-footer/PageFooter.js.map +1 -0
  78. package/lib/module/page-footer/PageFooterProvider.js +18 -0
  79. package/lib/module/page-footer/PageFooterProvider.js.map +1 -0
  80. package/lib/module/page-footer/context.js +7 -0
  81. package/lib/module/page-footer/context.js.map +1 -0
  82. package/lib/module/page-footer/index.js +6 -0
  83. package/lib/module/page-footer/index.js.map +1 -0
  84. package/lib/module/page-footer/types.js +4 -0
  85. package/lib/module/page-footer/types.js.map +1 -0
  86. package/lib/module/page-footer/use-page-footer-inset.js +16 -0
  87. package/lib/module/page-footer/use-page-footer-inset.js.map +1 -0
  88. package/lib/module/sidebar/Sidebar.js +6 -5
  89. package/lib/module/sidebar/Sidebar.js.map +1 -1
  90. package/lib/module/styles/panel-chrome.js +14 -1
  91. package/lib/module/styles/panel-chrome.js.map +1 -1
  92. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts +4 -0
  93. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/app-shell/types.d.ts +11 -3
  96. package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/appearance/BloomScope.d.ts +1 -1
  98. package/lib/typescript/commonjs/appearance/BloomScope.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/appearance/context.d.ts +2 -2
  100. package/lib/typescript/commonjs/appearance/context.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/appearance/types.d.ts +4 -1
  102. package/lib/typescript/commonjs/appearance/types.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts +3 -4
  104. package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts +1 -1
  106. package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/content-panel/types.d.ts +2 -2
  108. package/lib/typescript/commonjs/design-tokens/shapes.d.ts +2 -2
  109. package/lib/typescript/commonjs/index.d.ts +1 -0
  110. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  112. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/mail-thread/MailMessage.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/mail-thread/MailThread.d.ts +1 -1
  115. package/lib/typescript/commonjs/mail-thread/MailThread.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/mail-thread/shared.d.ts +1 -1
  117. package/lib/typescript/commonjs/mail-thread/shared.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/mail-thread/types.d.ts +2 -0
  119. package/lib/typescript/commonjs/mail-thread/types.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/page-footer/PageFooter.d.ts +7 -0
  121. package/lib/typescript/commonjs/page-footer/PageFooter.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/page-footer/PageFooterProvider.d.ts +4 -0
  123. package/lib/typescript/commonjs/page-footer/PageFooterProvider.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/page-footer/context.d.ts +3 -0
  125. package/lib/typescript/commonjs/page-footer/context.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/page-footer/index.d.ts +5 -0
  127. package/lib/typescript/commonjs/page-footer/index.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/page-footer/types.d.ts +26 -0
  129. package/lib/typescript/commonjs/page-footer/types.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/page-footer/use-page-footer-inset.d.ts +7 -0
  131. package/lib/typescript/commonjs/page-footer/use-page-footer-inset.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/sidebar/types.d.ts +1 -1
  134. package/lib/typescript/commonjs/styles/panel-chrome.d.ts +5 -0
  135. package/lib/typescript/commonjs/styles/panel-chrome.d.ts.map +1 -1
  136. package/lib/typescript/module/app-shell/AppShellBars.d.ts +4 -0
  137. package/lib/typescript/module/app-shell/AppShellBars.d.ts.map +1 -1
  138. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  139. package/lib/typescript/module/app-shell/types.d.ts +11 -3
  140. package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
  141. package/lib/typescript/module/appearance/BloomScope.d.ts +1 -1
  142. package/lib/typescript/module/appearance/BloomScope.d.ts.map +1 -1
  143. package/lib/typescript/module/appearance/context.d.ts +2 -2
  144. package/lib/typescript/module/appearance/context.d.ts.map +1 -1
  145. package/lib/typescript/module/appearance/types.d.ts +4 -1
  146. package/lib/typescript/module/appearance/types.d.ts.map +1 -1
  147. package/lib/typescript/module/content-panel/ContentPanel.d.ts +3 -4
  148. package/lib/typescript/module/content-panel/ContentPanel.d.ts.map +1 -1
  149. package/lib/typescript/module/content-panel/ContentPanel.web.d.ts +1 -1
  150. package/lib/typescript/module/content-panel/ContentPanel.web.d.ts.map +1 -1
  151. package/lib/typescript/module/content-panel/types.d.ts +2 -2
  152. package/lib/typescript/module/design-tokens/shapes.d.ts +2 -2
  153. package/lib/typescript/module/index.d.ts +1 -0
  154. package/lib/typescript/module/index.d.ts.map +1 -1
  155. package/lib/typescript/module/index.web.d.ts +1 -0
  156. package/lib/typescript/module/index.web.d.ts.map +1 -1
  157. package/lib/typescript/module/mail-thread/MailMessage.d.ts.map +1 -1
  158. package/lib/typescript/module/mail-thread/MailThread.d.ts +1 -1
  159. package/lib/typescript/module/mail-thread/MailThread.d.ts.map +1 -1
  160. package/lib/typescript/module/mail-thread/shared.d.ts +1 -1
  161. package/lib/typescript/module/mail-thread/shared.d.ts.map +1 -1
  162. package/lib/typescript/module/mail-thread/types.d.ts +2 -0
  163. package/lib/typescript/module/mail-thread/types.d.ts.map +1 -1
  164. package/lib/typescript/module/page-footer/PageFooter.d.ts +7 -0
  165. package/lib/typescript/module/page-footer/PageFooter.d.ts.map +1 -0
  166. package/lib/typescript/module/page-footer/PageFooterProvider.d.ts +4 -0
  167. package/lib/typescript/module/page-footer/PageFooterProvider.d.ts.map +1 -0
  168. package/lib/typescript/module/page-footer/context.d.ts +3 -0
  169. package/lib/typescript/module/page-footer/context.d.ts.map +1 -0
  170. package/lib/typescript/module/page-footer/index.d.ts +5 -0
  171. package/lib/typescript/module/page-footer/index.d.ts.map +1 -0
  172. package/lib/typescript/module/page-footer/types.d.ts +26 -0
  173. package/lib/typescript/module/page-footer/types.d.ts.map +1 -0
  174. package/lib/typescript/module/page-footer/use-page-footer-inset.d.ts +7 -0
  175. package/lib/typescript/module/page-footer/use-page-footer-inset.d.ts.map +1 -0
  176. package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
  177. package/lib/typescript/module/sidebar/types.d.ts +1 -1
  178. package/lib/typescript/module/styles/panel-chrome.d.ts +5 -0
  179. package/lib/typescript/module/styles/panel-chrome.d.ts.map +1 -1
  180. package/package.json +18 -1
  181. package/src/__tests__/support/adoption-matrix.ts +683 -0
  182. package/src/__tests__/support/card-surface.ts +25 -0
  183. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  184. package/src/__tests__/support/commerce-harness.tsx +97 -0
  185. package/src/__tests__/support/composite.ts +68 -0
  186. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  187. package/src/__tests__/support/messages-in.ts +10 -0
  188. package/src/__tests__/support/press-host.ts +30 -0
  189. package/src/__tests__/support/rendered-style.ts +99 -0
  190. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  191. package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
  192. package/src/agent-avatar/__tests__/artwork-golden.json +774 -0
  193. package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +26 -0
  194. package/src/app-shell/AppShell.tsx +2 -2
  195. package/src/app-shell/AppShellBars.tsx +6 -4
  196. package/src/app-shell/AppShellEngine.tsx +51 -18
  197. package/src/app-shell/types.ts +11 -3
  198. package/src/appearance/BloomScope.tsx +5 -2
  199. package/src/appearance/context.ts +2 -2
  200. package/src/appearance/types.ts +4 -1
  201. package/src/content-panel/ContentPanel.tsx +32 -25
  202. package/src/content-panel/ContentPanel.web.tsx +32 -23
  203. package/src/content-panel/types.ts +2 -2
  204. package/src/design-tokens/shapes.ts +1 -1
  205. package/src/design-tokens/tokens.json +2 -2
  206. package/src/index.ts +1 -0
  207. package/src/index.web.ts +1 -0
  208. package/src/mail-thread/MailMessage.tsx +9 -11
  209. package/src/mail-thread/MailThread.tsx +3 -1
  210. package/src/mail-thread/shared.ts +2 -1
  211. package/src/mail-thread/types.ts +2 -0
  212. package/src/page-footer/PageFooter.tsx +96 -0
  213. package/src/page-footer/PageFooterProvider.tsx +9 -0
  214. package/src/page-footer/context.ts +6 -0
  215. package/src/page-footer/index.ts +4 -0
  216. package/src/page-footer/types.ts +26 -0
  217. package/src/page-footer/use-page-footer-inset.ts +17 -0
  218. package/src/sidebar/Sidebar.tsx +5 -4
  219. package/src/sidebar/types.ts +1 -1
  220. package/src/styles/panel-chrome.ts +9 -1
  221. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
  222. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
  223. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
@@ -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": {
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';
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';
@@ -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. */
@@ -0,0 +1,96 @@
1
+ import React, { memo, useContext, useEffect, useId, useState } from 'react';
2
+ import { Platform, StyleSheet, View, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
3
+ import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
4
+ import { BloomScope } from '../appearance';
5
+ import { useBottomEdgeInset } from '../layout/bottom-edge';
6
+ import { EdgeScrim, SCRIM_TAIL_RATIO } from '../page-header/EdgeScrim';
7
+ import { BREAKPOINTS } from '../styles/breakpoints';
8
+ import { useSurfaceFill } from '../styles/surface-levels';
9
+ import { Z_INDEX } from '../styles/z-index';
10
+ import { useTheme } from '../theme/use-theme';
11
+ import { PageFooterContext } from './context';
12
+ import type { PageFooterProps } from './types';
13
+
14
+ const BAR_HEIGHT = 56;
15
+
16
+ /** Page-local controls floating over a bounded scroller, above app navigation. */
17
+ function PageFooterComponent({
18
+ children, actions, bottomInset, safeArea, scrim = 'always', scrimColor, style, testID,
19
+ }: PageFooterProps) {
20
+ const theme = useTheme();
21
+ const fill = useSurfaceFill() ?? theme.colors.background;
22
+ const occupied = useBottomEdgeInset();
23
+ const rawBottom = bottomInset ?? occupied;
24
+ const bottom = Number.isFinite(rawBottom) ? Math.max(0, rawBottom) : 0;
25
+ const insets = useContext(SafeAreaInsetsContext);
26
+ const padBottom = (safeArea ?? Platform.OS !== 'web') && bottom === 0 ? insets?.bottom ?? 0 : 0;
27
+ const windowWidth = useWindowDimensions().width;
28
+ const [width, setWidth] = useState<number | null>(null);
29
+ // Measure the row, not the outer box: a safe-area change can then update the
30
+ // published clearance immediately rather than waiting for another onLayout.
31
+ const [rowHeight, setRowHeight] = useState(BAR_HEIGHT);
32
+ const height = rowHeight + padBottom;
33
+ const sideInset = (width ?? windowWidth) >= BREAKPOINTS.sm ? 24 : 16;
34
+ const store = useContext(PageFooterContext);
35
+ const id = useId();
36
+ useEffect(() => {
37
+ store?.claim(id, height + bottom);
38
+ return () => store?.release(id);
39
+ }, [store, id, height, bottom]);
40
+
41
+ const onLayout = (event: LayoutChangeEvent) => {
42
+ const layout = event.nativeEvent.layout;
43
+ if (Number.isFinite(layout.width)) setWidth(Math.max(0, layout.width));
44
+ if (Number.isFinite(layout.height)) setRowHeight(Math.max(0, layout.height));
45
+ };
46
+
47
+ return (
48
+ <View
49
+ testID={testID}
50
+ pointerEvents="box-none"
51
+ style={[styles.container, { bottom, paddingBottom: padBottom }, style]}
52
+ >
53
+ {scrim !== 'none' ? (
54
+ <View
55
+ pointerEvents="none"
56
+ style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }]}
57
+ testID={testID ? `${testID}-scrim` : undefined}
58
+ >
59
+ <EdgeScrim edge="bottom" color={scrimColor ?? fill} testID={testID ? `${testID}-scrim-gradient` : undefined} />
60
+ </View>
61
+ ) : null}
62
+ <View
63
+ onLayout={onLayout}
64
+ pointerEvents="box-none"
65
+ style={[styles.row, { paddingLeft: sideInset, paddingRight: sideInset }]}
66
+ testID={testID ? `${testID}-row` : undefined}
67
+ >
68
+ <BloomScope>
69
+ {children != null ? <View pointerEvents="box-none" style={styles.content}>{children}</View> : null}
70
+ {actions != null ? (
71
+ <View pointerEvents="box-none" style={styles.actions} testID={testID ? `${testID}-actions` : undefined}>
72
+ {actions}
73
+ </View>
74
+ ) : null}
75
+ </BloomScope>
76
+ </View>
77
+ </View>
78
+ );
79
+ }
80
+
81
+ const styles = StyleSheet.create({
82
+ container: {
83
+ position: 'absolute', left: 0, right: 0, zIndex: Z_INDEX.floating,
84
+ ...(Platform.OS !== 'web' ? { elevation: Z_INDEX.floating } : {}),
85
+ },
86
+ scrim: { position: 'absolute', bottom: 0, left: 0, right: 0 },
87
+ row: {
88
+ minHeight: BAR_HEIGHT, flexDirection: 'row', alignItems: 'center',
89
+ gap: 8, paddingTop: 8, paddingBottom: 8,
90
+ },
91
+ content: { flex: 1, minWidth: 0 },
92
+ actions: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 8, flexShrink: 1 },
93
+ });
94
+
95
+ export const PageFooter = memo(PageFooterComponent);
96
+ PageFooter.displayName = 'PageFooter';
@@ -0,0 +1,9 @@
1
+ import React, { useState, type PropsWithChildren } from 'react';
2
+ import { createEdgeStore } from '../layout/edge-store';
3
+ import { PageFooterContext } from './context';
4
+
5
+ /** Scope a page's footer measurement to its sibling scroller. Adds no layout. */
6
+ export function PageFooterProvider({ children }: PropsWithChildren) {
7
+ const [store] = useState(createEdgeStore);
8
+ return <PageFooterContext.Provider value={store}>{children}</PageFooterContext.Provider>;
9
+ }
@@ -0,0 +1,6 @@
1
+ import { createContext } from 'react';
2
+ import type { EdgeStore } from '../layout/edge-store';
3
+
4
+ // Local scroll clearance, deliberately separate from the global bottom edge
5
+ // that positions this footer. Publishing there would make it avoid itself.
6
+ export const PageFooterContext = createContext<EdgeStore | null>(null);
@@ -0,0 +1,4 @@
1
+ export { PageFooter } from './PageFooter';
2
+ export { PageFooterProvider } from './PageFooterProvider';
3
+ export { usePageFooterInset } from './use-page-footer-inset';
4
+ export type { PageFooterProps } from './types';
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface PageFooterProps {
5
+ /** Optional content before the actions, for example a selection summary. */
6
+ children?: ReactNode;
7
+ /** Controls grouped by the caller; inherits the surrounding Bloom size. */
8
+ actions?: ReactNode;
9
+ /**
10
+ * Distance from the containing frame's bottom. Defaults to the occupied
11
+ * bottom edge (for example AppShell's measured bottom bar). Pass 0 when the
12
+ * frame already ends above navigation, or the footer is in a desktop pane.
13
+ */
14
+ bottomInset?: number;
15
+ /**
16
+ * Include the bottom safe area when no bottom edge is occupied. Defaults to
17
+ * true on native, false on web. Disable when the containing shell owns it.
18
+ */
19
+ safeArea?: boolean;
20
+ /** The PageHeader gradient, mirrored upward. Default always. */
21
+ scrim?: 'always' | 'none';
22
+ /** Opaque gradient color; defaults to the containing surface's fill. */
23
+ scrimColor?: string;
24
+ style?: StyleProp<ViewStyle>;
25
+ testID?: string;
26
+ }
@@ -0,0 +1,17 @@
1
+ import { useContext, useSyncExternalStore } from 'react';
2
+ import { NO_INSET, NO_SUBSCRIPTION } from '../layout/edge-store';
3
+ import { PageFooterContext } from './context';
4
+
5
+ /**
6
+ * Clearance for the last scroll item: measured footer height plus bottomInset.
7
+ * Use as contentContainerStyle.paddingBottom. Returns 0 without a provider or
8
+ * once the footer unmounts; does not include the decorative gradient tail.
9
+ */
10
+ export function usePageFooterInset(): number {
11
+ const store = useContext(PageFooterContext);
12
+ return useSyncExternalStore(
13
+ store?.subscribe ?? NO_SUBSCRIPTION,
14
+ store?.getInset ?? NO_INSET,
15
+ store?.getInset ?? NO_INSET,
16
+ );
17
+ }
@@ -3,7 +3,7 @@ import { resolveSurfaceMaterial } from '../surface/resolve-surface-material';
3
3
  import { SurfacePaint } from '../surface/SurfacePaint';
4
4
  import { parseRgba } from '../theme/color-utils';
5
5
  import { surfaceStyle } from '../shapes/surface-style';
6
- import { SURFACE_SHAPES } from '../design-tokens/shapes';
6
+ import { usePanelShape } from '../styles/panel-chrome';
7
7
  import { useBloomAppearance } from '../appearance';
8
8
  import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
9
  import {
@@ -62,7 +62,7 @@ import { SIDEBAR_MESSAGES } from './messages';
62
62
  * panel `size` wide expanded (p12), its collapsed width at px7 py12 —
63
63
  * with the 1px border that leaves exactly the size's item column
64
64
  * (`metrics.ts`; `md` is the historical 260 / 52 / 36)
65
- * surface `card` radius 24, 1px border-button-white, shadow-sidebar,
65
+ * surface `card` shared panel radius, 1px border-button-white, shadow-sidebar,
66
66
  * background-secondary; `plain` drops the chrome onto
67
67
  * background-full; `docked` keeps the fill, squares the corners
68
68
  * and leaves one hairline on the inner edge, full height
@@ -219,6 +219,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
219
219
  const palette = useSidebarPalette();
220
220
  const parentFill = useSurfaceFill();
221
221
  const parentLevel = useSurfaceLevelValue();
222
+ const panelShape = usePanelShape();
222
223
  const customSurface = StyleSheet.flatten(style);
223
224
  const surfaceFill = customSurface?.backgroundColor ?? (surface === 'plain' ? undefined : (palette.panel));
224
225
  const hasFill = typeof surfaceFill === 'string' && surfaceFill !== 'transparent' && parseRgba(surfaceFill)?.a !== 0;
@@ -444,7 +445,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
444
445
  borderEndColor: palette.dockedEdge,
445
446
  }
446
447
  : {
447
- ...surfaceStyle(SURFACE_SHAPES.sidebar),
448
+ ...surfaceStyle(panelShape),
448
449
  borderWidth: 1,
449
450
  borderColor: palette.panelBorder,
450
451
  backgroundColor: 'transparent',
@@ -709,7 +710,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
709
710
  surfaceFillVars(publishedFill),
710
711
  ]}
711
712
  >
712
- {paintsSurface ? <SurfacePaint fill={surfaceFill} shape={{ curve: SURFACE_SHAPES.sidebar.curve }} radius={customSurface?.borderRadius ?? (surface === 'card' ? 24 : 0)} /> : null}
713
+ {paintsSurface ? <SurfacePaint fill={surfaceFill} shape={{ curve: panelShape.curve }} radius={customSurface?.borderRadius ?? (surface === 'card' ? panelShape.radius : 0)} /> : null}
713
714
  <View testID={`${testID ?? 'sidebar'}-main-region`} style={{ width: '100%', minHeight: 0, flexShrink: 1, ...(contentAlignment === 'center' ? { flex: 1, justifyContent: 'center' as const } : {}) }}>
714
715
  <View style={{ width: '100%', minHeight: 0, flexShrink: 1 }}>
715
716
  {/* Fixed chrome shares the panel's morph, never the destination scroll.
@@ -151,7 +151,7 @@ export type SidebarVariant = 'panel' | 'rail';
151
151
  * page, independent of what it contains. It is the axis two apps differ on
152
152
  * most: the same rows read as a different product behind a different edge.
153
153
  *
154
- * - `card` (default) the floating panel: radius 24, a hairline and the panel
154
+ * - `card` (default) the floating panel: shared panel radius (28 by default), a hairline and the panel
155
155
  * shadow, on the panel fill. A sidebar that sits IN the page.
156
156
  * - `plain` no edge at all — no radius, no border, no shadow: the rows sit
157
157
  * straight on the page. The social/reader look, and what a revealed mobile
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The FLOATING PANEL chrome — the edge Bloom's big surfaces wear: a hairline
3
- * plus the soft three-part shadow that lifts a panel off the page background.
3
+ * plus the shared corner shape and soft three-part shadow that lifts a panel off the page background.
4
4
  *
5
5
  * It lives here because two surfaces wear the same thing and had it written
6
6
  * twice: the `Sidebar`'s panel and the framed `ContentPanel`. A panel that only
@@ -12,6 +12,8 @@
12
12
  * border the canonical subtle border role in both modes
13
13
  */
14
14
  import { useContext } from 'react';
15
+ import { BloomAppearanceContext } from '../appearance/context';
16
+ import { SURFACE_SHAPES } from '../design-tokens/shapes';
15
17
 
16
18
  import { BloomThemeContext } from '../theme/BloomThemeProvider';
17
19
  import type { Theme } from '../theme/types';
@@ -51,3 +53,9 @@ export function useOptionalPanelChrome(): PanelChrome | null {
51
53
  const ctx = useContext(BloomThemeContext);
52
54
  return ctx ? resolvePanelChrome(ctx.theme) : null;
53
55
  }
56
+
57
+ /** Shared panel geometry, independently configurable from control size. */
58
+ export function usePanelShape() {
59
+ const { panelRadius } = useContext(BloomAppearanceContext);
60
+ return { ...SURFACE_SHAPES.sidebar, radius: panelRadius ?? SURFACE_SHAPES.sidebar.radius };
61
+ }