@oxyhq/bloom 1.9.2 → 1.10.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 (123) hide show
  1. package/docs/layout.mdx +79 -0
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +14 -4
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  4. package/lib/commonjs/fab/Fab.js +12 -3
  5. package/lib/commonjs/fab/Fab.js.map +1 -1
  6. package/lib/commonjs/fab/Fab.web.js +12 -4
  7. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  8. package/lib/commonjs/index.js +137 -101
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/index.web.js +137 -101
  11. package/lib/commonjs/index.web.js.map +1 -1
  12. package/lib/commonjs/layout/bottom-edge.js +148 -0
  13. package/lib/commonjs/layout/bottom-edge.js.map +1 -0
  14. package/lib/commonjs/layout/edge.js +67 -0
  15. package/lib/commonjs/layout/edge.js.map +1 -0
  16. package/lib/commonjs/layout/index.js +38 -0
  17. package/lib/commonjs/layout/index.js.map +1 -0
  18. package/lib/commonjs/provider/BloomProvider.js +5 -2
  19. package/lib/commonjs/provider/BloomProvider.js.map +1 -1
  20. package/lib/commonjs/tab-bar/TabBarBase.js +11 -1
  21. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  22. package/lib/commonjs/tab-bar/shared.js +0 -22
  23. package/lib/commonjs/tab-bar/shared.js.map +1 -1
  24. package/lib/commonjs/tab-bar/use-footprint.js +17 -8
  25. package/lib/commonjs/tab-bar/use-footprint.js.map +1 -1
  26. package/lib/commonjs/toast/Positioner.js +6 -1
  27. package/lib/commonjs/toast/Positioner.js.map +1 -1
  28. package/lib/commonjs/toast/container-geometry.js +19 -13
  29. package/lib/commonjs/toast/container-geometry.js.map +1 -1
  30. package/lib/module/bottom-sheet/BottomSheetBase.js +14 -4
  31. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  32. package/lib/module/fab/Fab.js +12 -3
  33. package/lib/module/fab/Fab.js.map +1 -1
  34. package/lib/module/fab/Fab.web.js +12 -4
  35. package/lib/module/fab/Fab.web.js.map +1 -1
  36. package/lib/module/index.js +1 -0
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +1 -0
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/layout/bottom-edge.js +141 -0
  41. package/lib/module/layout/bottom-edge.js.map +1 -0
  42. package/lib/module/layout/edge.js +62 -0
  43. package/lib/module/layout/edge.js.map +1 -0
  44. package/lib/module/layout/index.js +11 -0
  45. package/lib/module/layout/index.js.map +1 -0
  46. package/lib/module/provider/BloomProvider.js +5 -2
  47. package/lib/module/provider/BloomProvider.js.map +1 -1
  48. package/lib/module/tab-bar/TabBarBase.js +12 -2
  49. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  50. package/lib/module/tab-bar/shared.js +0 -21
  51. package/lib/module/tab-bar/shared.js.map +1 -1
  52. package/lib/module/tab-bar/use-footprint.js +18 -9
  53. package/lib/module/tab-bar/use-footprint.js.map +1 -1
  54. package/lib/module/toast/Positioner.js +6 -1
  55. package/lib/module/toast/Positioner.js.map +1 -1
  56. package/lib/module/toast/container-geometry.js +20 -13
  57. package/lib/module/toast/container-geometry.js.map +1 -1
  58. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/index.d.ts +1 -0
  62. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  64. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/layout/bottom-edge.d.ts +77 -0
  66. package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/layout/edge.d.ts +56 -0
  68. package/lib/typescript/commonjs/layout/edge.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/layout/index.d.ts +9 -0
  70. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/tab-bar/shared.d.ts +0 -9
  74. package/lib/typescript/commonjs/tab-bar/shared.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +11 -7
  76. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/toast/Positioner.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/toast/container-geometry.d.ts +14 -5
  79. package/lib/typescript/commonjs/toast/container-geometry.d.ts.map +1 -1
  80. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  81. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  82. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  83. package/lib/typescript/module/index.d.ts +1 -0
  84. package/lib/typescript/module/index.d.ts.map +1 -1
  85. package/lib/typescript/module/index.web.d.ts +1 -0
  86. package/lib/typescript/module/index.web.d.ts.map +1 -1
  87. package/lib/typescript/module/layout/bottom-edge.d.ts +77 -0
  88. package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -0
  89. package/lib/typescript/module/layout/edge.d.ts +56 -0
  90. package/lib/typescript/module/layout/edge.d.ts.map +1 -0
  91. package/lib/typescript/module/layout/index.d.ts +9 -0
  92. package/lib/typescript/module/layout/index.d.ts.map +1 -0
  93. package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
  94. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  95. package/lib/typescript/module/tab-bar/shared.d.ts +0 -9
  96. package/lib/typescript/module/tab-bar/shared.d.ts.map +1 -1
  97. package/lib/typescript/module/tab-bar/use-footprint.d.ts +11 -7
  98. package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -1
  99. package/lib/typescript/module/toast/Positioner.d.ts.map +1 -1
  100. package/lib/typescript/module/toast/container-geometry.d.ts +14 -5
  101. package/lib/typescript/module/toast/container-geometry.d.ts.map +1 -1
  102. package/package.json +15 -1
  103. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  104. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  105. package/src/__tests__/support/press-host.ts +30 -0
  106. package/src/__tests__/support/rendered-style.ts +99 -0
  107. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  108. package/src/bottom-sheet/BottomSheetBase.tsx +14 -4
  109. package/src/fab/Fab.tsx +16 -3
  110. package/src/fab/Fab.web.tsx +16 -4
  111. package/src/index.ts +7 -0
  112. package/src/index.web.ts +7 -0
  113. package/src/layout/bottom-edge.tsx +162 -0
  114. package/src/layout/edge.ts +59 -0
  115. package/src/layout/index.ts +8 -0
  116. package/src/provider/BloomProvider.tsx +11 -1
  117. package/src/tab-bar/TabBarBase.tsx +11 -2
  118. package/src/tab-bar/shared.ts +0 -20
  119. package/src/tab-bar/use-footprint.ts +18 -9
  120. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +7682 -0
  121. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  122. package/src/toast/Positioner.tsx +5 -0
  123. package/src/toast/container-geometry.ts +20 -8
package/src/index.web.ts CHANGED
@@ -158,6 +158,13 @@ export type {
158
158
  SurfacePromptOptions,
159
159
  } from './surfaces/index.web';
160
160
  export * from './button/index.web';
161
+ export {
162
+ EDGE_GAP,
163
+ windowEdgeGap,
164
+ BottomEdgeProvider,
165
+ useBottomEdgeInset,
166
+ useClaimBottomEdge,
167
+ } from './layout';
161
168
  export { Fab } from './fab/index.web';
162
169
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab/index.web';
163
170
  export { FrostedIconButton } from './frosted-icon-button/index.web';
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The bottom edge's OCCUPANCY — how much of it is already claimed by a floating
3
+ * surface, so the next surface can stack above it instead of underneath it.
4
+ *
5
+ * ## The problem
6
+ *
7
+ * `windowEdgeGap` tells a surface where the window edge is. It cannot tell it
8
+ * that a tab bar is already parked there. Nothing could, so nothing did: Bloom's
9
+ * `Fab` anchored itself 16px off the edge and landed squarely behind the
10
+ * floating tab bar, and every app that hit it patched around Bloom instead —
11
+ * Mention grew a `BottomBarAwareFab` that read `useTabBarFootprint()` and lifted
12
+ * the FAB by hand, on web only, leaving native broken across seven screens.
13
+ *
14
+ * A z-index cannot rescue that placement either. The tab bar host is the last
15
+ * sibling of the app shell, so it paints above every descendant regardless of
16
+ * what they claim; the FAB has to be somewhere ELSE, not merely on top.
17
+ *
18
+ * ## The model
19
+ *
20
+ * A surface that parks at the bottom edge CLAIMS its footprint. A surface that
21
+ * wants to avoid it READS the total and offsets itself. Neither one imports the
22
+ * other, so a consumer composes them in any combination — a tab bar with no FAB,
23
+ * a FAB with no tab bar, both, or a third surface written later — and the
24
+ * geometry still resolves.
25
+ *
26
+ * Claims combine with MAX, not sum. Every claim is measured from the same window
27
+ * edge, so two surfaces at that edge overlap rather than stack; the tallest is
28
+ * what a new surface has to clear. Summing would strand it at twice the height.
29
+ *
30
+ * ## Why an external store
31
+ *
32
+ * The claim set is mutable state living outside React. Reading it in a memoized
33
+ * position is exactly the stale-read the React Compiler produces, so it is read
34
+ * through `useSyncExternalStore` and never touched directly. The store is
35
+ * created per provider (not at module scope) so tests, and any app mounting two
36
+ * roots, stay isolated from each other.
37
+ */
38
+ import {
39
+ createContext,
40
+ useContext,
41
+ useEffect,
42
+ useId,
43
+ useState,
44
+ useSyncExternalStore,
45
+ type PropsWithChildren,
46
+ } from 'react';
47
+
48
+ interface BottomEdgeStore {
49
+ subscribe: (onChange: () => void) => () => void;
50
+ /**
51
+ * The cached total. Returns the SAME number until a claim actually changes it
52
+ * — `useSyncExternalStore` re-renders forever if the snapshot is recomputed
53
+ * per call.
54
+ */
55
+ getInset: () => number;
56
+ claim: (id: string, height: number) => void;
57
+ release: (id: string) => void;
58
+ }
59
+
60
+ function createBottomEdgeStore(): BottomEdgeStore {
61
+ const claims = new Map<string, number>();
62
+ const listeners = new Set<() => void>();
63
+ let inset = 0;
64
+
65
+ const recompute = () => {
66
+ let next = 0;
67
+ for (const height of claims.values()) {
68
+ if (height > next) next = height;
69
+ }
70
+ // Bail before notifying: a re-registration at an unchanged height (every
71
+ // render of a claimant whose footprint did not move) must not re-render
72
+ // every reader.
73
+ if (next === inset) return;
74
+ inset = next;
75
+ for (const listener of listeners) listener();
76
+ };
77
+
78
+ return {
79
+ subscribe(onChange) {
80
+ listeners.add(onChange);
81
+ return () => {
82
+ listeners.delete(onChange);
83
+ };
84
+ },
85
+ getInset: () => inset,
86
+ claim(id, height) {
87
+ if (claims.get(id) === height) return;
88
+ claims.set(id, height);
89
+ recompute();
90
+ },
91
+ release(id) {
92
+ if (!claims.delete(id)) return;
93
+ recompute();
94
+ },
95
+ };
96
+ }
97
+
98
+ const BottomEdgeContext = createContext<BottomEdgeStore | null>(null);
99
+
100
+ /**
101
+ * Wrap the app once — `BloomProvider` already does. Rendering a second one
102
+ * scopes a nested subtree to its own claims, which is a real (if rare) thing to
103
+ * want: a bottom-edge surface inside a full-screen modal should not be offset by
104
+ * the tab bar the modal is covering.
105
+ */
106
+ export function BottomEdgeProvider({ children }: PropsWithChildren) {
107
+ const [store] = useState(createBottomEdgeStore);
108
+ return <BottomEdgeContext.Provider value={store}>{children}</BottomEdgeContext.Provider>;
109
+ }
110
+
111
+ BottomEdgeProvider.displayName = 'BottomEdgeProvider';
112
+
113
+ // Stable module-scope identities for the no-provider path. Fresh closures here
114
+ // would resubscribe `useSyncExternalStore` on every render.
115
+ const NO_SUBSCRIPTION = () => () => {};
116
+ const NO_INSET = () => 0;
117
+
118
+ /**
119
+ * How much of the bottom edge is already occupied, in px.
120
+ *
121
+ * Add it to whatever offset the surface would otherwise use:
122
+ *
123
+ * ```tsx
124
+ * const occupied = useBottomEdgeInset();
125
+ * <View style={{ position: 'absolute', bottom: windowEdgeGap(insets.bottom) + occupied }} />
126
+ * ```
127
+ *
128
+ * `0` outside a provider, and `0` on the first commit even inside one — a claim
129
+ * registers in an effect, so a reader mounted in the same commit as its claimant
130
+ * settles one frame later. That is a layout offset rather than a measurement, so
131
+ * the settle is invisible in practice; nothing here waits on a real layout pass.
132
+ */
133
+ export function useBottomEdgeInset(): number {
134
+ const store = useContext(BottomEdgeContext);
135
+ return useSyncExternalStore(
136
+ store?.subscribe ?? NO_SUBSCRIPTION,
137
+ store?.getInset ?? NO_INSET,
138
+ store?.getInset ?? NO_INSET,
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Claim `height` px of the bottom edge for as long as the caller is mounted.
144
+ *
145
+ * The claimant owns its own placement — claiming does not move it. It declares
146
+ * the space it occupies so that everything reading `useBottomEdgeInset()` stays
147
+ * off it. Pass the FULL footprint (the surface's height plus the gap it holds
148
+ * off the window edge), which is the same number the surface positions itself
149
+ * with.
150
+ *
151
+ * A no-op outside a provider, so a surface stays usable standalone.
152
+ */
153
+ export function useClaimBottomEdge(height: number): void {
154
+ const store = useContext(BottomEdgeContext);
155
+ const id = useId();
156
+
157
+ useEffect(() => {
158
+ if (!store) return;
159
+ store.claim(id, height);
160
+ return () => store.release(id);
161
+ }, [store, id, height]);
162
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * How far a floating surface sits from a window edge.
3
+ *
4
+ * ## The rule this file encodes
5
+ *
6
+ * A floating surface CLEARS THE OS'S RESERVED BAND IN FULL. The safe-area inset
7
+ * is the space the platform has reserved for its own affordance — the iOS home
8
+ * indicator, the Android gesture handle — and a surface that sits inside it is
9
+ * sitting on top of a system control.
10
+ *
11
+ * Bloom used to answer this question three times, differently:
12
+ *
13
+ * - a detached `BottomSheet` `insets.bottom + 16`
14
+ * - the toast stack `bottom > 0 ? bottom + 8 : 16`
15
+ * - the floating tab bar `max(bottom - 16, 12)`
16
+ *
17
+ * The tab bar was the only one that SUBTRACTED, and subtracting is what put it
18
+ * on the Android gesture handle: a gesture-navigation device reserves ~24dp, the
19
+ * bar absorbed 16 of it, hit its own 12dp floor, and landed inside the band with
20
+ * the handle drawn through it. The other two were never reachable by that bug
21
+ * because they add.
22
+ *
23
+ * One function now answers it for all three, and the additive `gap` reproduces
24
+ * the sheet and the toast exactly — only the tab bar moves.
25
+ */
26
+
27
+ /**
28
+ * Breathing room from a window edge where the platform reserves nothing at all
29
+ * (web, a browser with no visual viewport inset, an Android device with the
30
+ * gesture bar hidden).
31
+ *
32
+ * It is also the FLOOR for every surface: a small-but-nonzero inset must never
33
+ * pull a surface closer to the edge than a device reporting no inset would.
34
+ */
35
+ export const EDGE_GAP = 16;
36
+
37
+ /**
38
+ * The distance a floating surface holds off a window edge, given that edge's
39
+ * safe-area inset.
40
+ *
41
+ * `gap` is the surface's own breathing room ON TOP of the reserved band, not an
42
+ * allowance to spend inside it — a bigger `gap` floats the surface further from
43
+ * the affordance, never closer. Pass `0` for a surface that should hug the safe
44
+ * area (the tab bar), a positive number for one that should stand clear of it (a
45
+ * detached sheet, a toast stack).
46
+ *
47
+ * ```ts
48
+ * windowEdgeGap(insets.bottom) // 34pt iOS · 24dp Android · 16 web
49
+ * windowEdgeGap(insets.bottom, 16) // 50pt iOS · 40dp Android · 16 web
50
+ * ```
51
+ *
52
+ * Anything laying out AROUND such a surface wants its total footprint (this gap
53
+ * plus the surface's own height) and must get it from the surface — never by
54
+ * adding `insets.bottom` to a hardcoded height, which double-counts the inset
55
+ * because the inset is already folded in here.
56
+ */
57
+ export function windowEdgeGap(inset: number, gap = 0): number {
58
+ return Math.max(inset + gap, EDGE_GAP);
59
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Window-edge geometry, shared by every floating surface.
3
+ *
4
+ * `edge` answers "how far from the edge"; `bottom-edge` answers "what is already
5
+ * parked there". A surface that floats at the bottom needs both.
6
+ */
7
+ export { EDGE_GAP, windowEdgeGap } from './edge';
8
+ export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
@@ -34,6 +34,7 @@
34
34
  import type { ReactNode } from 'react';
35
35
 
36
36
  import { GlassBlurTargetProvider } from '../glass/blur-target';
37
+ import { BottomEdgeProvider } from '../layout/bottom-edge';
37
38
  import { ImageResolverProvider, type ImageResolver } from '../image-resolver';
38
39
  import { BloomHapticsProvider } from '../hooks/use-haptics';
39
40
  import { TabBarMinimizeProvider } from '../tab-bar/context';
@@ -67,7 +68,16 @@ export function BloomProvider({
67
68
  provider already does with `children`.
68
69
  */}
69
70
  <GlassBlurTargetProvider>
70
- <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
71
+ {/*
72
+ The bottom edge's claim registry. Also a provider, not an outlet:
73
+ it renders nothing and holds only the set of surfaces currently
74
+ parked at the bottom edge, so a tab bar can publish its footprint
75
+ and a FAB or a toast stack can read it without either importing
76
+ the other.
77
+ */}
78
+ <BottomEdgeProvider>
79
+ <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
80
+ </BottomEdgeProvider>
71
81
  </GlassBlurTargetProvider>
72
82
  </BloomHapticsProvider>
73
83
  </ScrollRestorationProvider>
@@ -38,6 +38,8 @@ import Animated, {
38
38
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
39
39
 
40
40
  import { useHaptics } from '../hooks/use-haptics';
41
+ import { useClaimBottomEdge } from '../layout/bottom-edge';
42
+ import { windowEdgeGap } from '../layout/edge';
41
43
  import type { ProgressiveBlurProps } from '../progressive-blur/types';
42
44
  import { setMinimized, useMinimizeState } from './context';
43
45
  import {
@@ -56,7 +58,6 @@ import {
56
58
  MINIMIZED_INSET,
57
59
  ROW_PAD_H,
58
60
  SLIDE_SPRING,
59
- tabBarBottomGap,
60
61
  useTabBarTheme,
61
62
  type TabBarGlyphProps,
62
63
  type TabBarSurfaceProps,
@@ -419,7 +420,15 @@ function TabBarBody({
419
420
 
420
421
  // Shared with `useTabBarFootprint`, so a consumer accounting for the bar in
421
422
  // its own layout can never drift from where the bar actually sits.
422
- const bottomOffset = tabBarBottomGap(insets.bottom);
423
+ const bottomOffset = windowEdgeGap(insets.bottom);
424
+
425
+ // Publish what the bar occupies so anything else at this edge stacks above it
426
+ // rather than behind it. Same number `useTabBarFootprint` reports, derived from
427
+ // the same two values, so a consumer reading either can never disagree with
428
+ // where the bar actually sits. The EXPANDED height on purpose: the bar
429
+ // minimizes on scroll and re-expands, so claiming the minimized height would
430
+ // drop a FAB onto the pill the moment the user scrolled back up.
431
+ useClaimBottomEdge(bottomOffset + EXPANDED_HEIGHT);
423
432
 
424
433
  // How centring and the animated inset compose: centring is STATIC and belongs
425
434
  // to the wrap, the inset stays ANIMATED on the pill inside it. The wrap is
@@ -51,26 +51,6 @@ export const BLUR_BLEED = 44;
51
51
  /** Label type size. Small by design — the icon carries the meaning. */
52
52
  export const LABEL_FONT_SIZE = 9.5;
53
53
 
54
- /**
55
- * How much of the bottom safe-area inset the bar's own gap absorbs. The pill
56
- * already floats clear of the content, so sitting the FULL inset below it
57
- * strands it in the middle of the screen on a home-indicator device.
58
- */
59
- const SAFE_AREA_ABSORBED = 16;
60
- /** Gap floor, for a device (or a browser) reporting no bottom inset at all. */
61
- const MIN_BOTTOM_GAP = 12;
62
-
63
- /**
64
- * Vertical gap between the bottom of the pill and the bottom of the window.
65
- *
66
- * The safe-area inset is folded in HERE, which is why anything accounting for
67
- * the bar in its own layout must go through `useTabBarFootprint` (which derives
68
- * its number from this same function) rather than adding `insets.bottom` to a
69
- * hardcoded height — that double-counts the inset.
70
- */
71
- export function tabBarBottomGap(bottomInset: number): number {
72
- return Math.max(bottomInset - SAFE_AREA_ABSORBED, MIN_BOTTOM_GAP);
73
- }
74
54
 
75
55
  /**
76
56
  * Slide spring: interruptible by design — rapid tab-hopping retargets with
@@ -13,7 +13,8 @@
13
13
  */
14
14
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
15
 
16
- import { EXPANDED_HEIGHT, tabBarBottomGap } from './shared';
16
+ import { windowEdgeGap } from '../layout/edge';
17
+ import { EXPANDED_HEIGHT } from './shared';
17
18
 
18
19
  /**
19
20
  * The vertical space the tab bar OCCUPIES above the bottom of the window, in
@@ -30,15 +31,19 @@ import { EXPANDED_HEIGHT, tabBarBottomGap } from './shared';
30
31
  * const footprint = useTabBarFootprint();
31
32
  *
32
33
  * <Animated.ScrollView contentContainerStyle={{ paddingBottom: footprint + 12 }} />
33
- * <Fab style={{ position: 'absolute', right: 16, bottom: footprint }} />
34
34
  * ```
35
35
  *
36
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
37
- * inset into its own gap, so this is measured from the true window edge, not
38
- * from the safe area. Never write `footprint + insets.bottom`: that counts the
39
- * home indicator twice and strands a visible band of dead space under every
40
- * list (114px accounted for a 76px bar on an iOS device or PWA). Whatever you
41
- * add on top is your own clearance — never the inset a second time.
36
+ * A FAB no longer needs this hook at all `Fab` reads the bottom edge's
37
+ * occupancy itself (see `layout/bottom-edge`), which is what the bar publishes
38
+ * this exact number to. Reach for the hook when laying out something that is not
39
+ * a Bloom surface.
40
+ *
41
+ * The value ALREADY INCLUDES the bottom safe-area inset `windowEdgeGap` clears
42
+ * the OS's reserved band in full — so this is measured from the true window
43
+ * edge, not from the safe area. Never write `footprint + insets.bottom`: that
44
+ * counts the home indicator twice and strands a visible band of dead space under
45
+ * every list. Whatever you add on top is your own clearance, never the inset a
46
+ * second time.
42
47
  *
43
48
  * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
44
49
  * down and re-expands on the way up, so measuring the minimized height would
@@ -46,5 +51,9 @@ import { EXPANDED_HEIGHT, tabBarBottomGap } from './shared';
46
51
  */
47
52
  export function useTabBarFootprint(): number {
48
53
  const insets = useSafeAreaInsets();
49
- return tabBarBottomGap(insets.bottom) + EXPANDED_HEIGHT;
54
+ // Gap 0: the bar hugs the safe area, the tightest any Bloom surface floats. It
55
+ // is a compact pill, not a panel, and a detached-sheet-sized gap under it would
56
+ // strand it mid-screen. It used to ABSORB 16px of the inset and clamp at a 12px
57
+ // floor, which is how it ended up drawn through the Android gesture handle.
58
+ return windowEdgeGap(insets.bottom) + EXPANDED_HEIGHT;
50
59
  }