@oxyhq/bloom 1.11.0 → 1.13.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 (41) hide show
  1. package/docs/layout.mdx +20 -29
  2. package/lib/commonjs/fab/Fab.js +0 -42
  3. package/lib/commonjs/fab/Fab.js.map +1 -1
  4. package/lib/commonjs/fab/Fab.web.js +3 -18
  5. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  6. package/lib/commonjs/layout/bottom-edge.js +42 -68
  7. package/lib/commonjs/layout/bottom-edge.js.map +1 -1
  8. package/lib/commonjs/layout/index.js +0 -6
  9. package/lib/commonjs/layout/index.js.map +1 -1
  10. package/lib/commonjs/tab-bar/TabBarBase.js +6 -22
  11. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  12. package/lib/module/fab/Fab.js +2 -44
  13. package/lib/module/fab/Fab.js.map +1 -1
  14. package/lib/module/fab/Fab.web.js +4 -19
  15. package/lib/module/fab/Fab.web.js.map +1 -1
  16. package/lib/module/layout/bottom-edge.js +42 -67
  17. package/lib/module/layout/bottom-edge.js.map +1 -1
  18. package/lib/module/layout/index.js +1 -1
  19. package/lib/module/layout/index.js.map +1 -1
  20. package/lib/module/tab-bar/TabBarBase.js +8 -24
  21. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  22. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/layout/bottom-edge.d.ts +28 -40
  25. package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/layout/index.d.ts +1 -1
  27. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  29. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  30. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  31. package/lib/typescript/module/layout/bottom-edge.d.ts +28 -40
  32. package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
  33. package/lib/typescript/module/layout/index.d.ts +1 -1
  34. package/lib/typescript/module/layout/index.d.ts.map +1 -1
  35. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/fab/Fab.tsx +3 -41
  38. package/src/fab/Fab.web.tsx +4 -19
  39. package/src/layout/bottom-edge.tsx +49 -84
  40. package/src/layout/index.ts +1 -6
  41. package/src/tab-bar/TabBarBase.tsx +6 -31
@@ -27,20 +27,27 @@
27
27
  * edge, so two surfaces at that edge overlap rather than stack; the tallest is
28
28
  * what a new surface has to clear. Summing would strand it at twice the height.
29
29
  *
30
- * ## Two numbers, because there are two questions
30
+ * ## What does NOT belong here: the collapse
31
31
  *
32
- * A surface that collapses — the tab bar minimizes 58 -> 44 on scroll occupies
33
- * less than it reserves. Readers want different halves of that:
32
+ * The tab bar minimizes 58 -> 44 on scroll, and this registry twice tried to
33
+ * publish that so a FAB could react first as a live pixel height (1.11.0), then
34
+ * as a boolean (1.12.0). Both felt wrong on a device, for the same reason, and
35
+ * the reason is structural rather than a matter of tuning:
34
36
  *
35
- * - RESERVED (`useBottomEdgeInset`) never shrinks while the claimant is
36
- * mounted. It is what must be kept permanently free: a list's bottom
37
- * padding, a toast stack's offset. Tracking the collapse here would jitter a
38
- * list's content size on every scroll and jump a toast 14px mid-display.
39
- * - LIVE (`useBottomEdgeLiveInset`) follows the collapse. It is what a surface
40
- * SITTING ON the edge wants, so it rides down with the bar instead of
41
- * leaving a hole.
37
+ * the bar animates on the UI thread, while anything reading it from here
38
+ * arrives via `runOnJS` plus two React render passes.
42
39
  *
43
- * A claimant that never collapses passes one number and both answers agree.
40
+ * So a consumer's reaction STARTS one to three frames after the bar's, and the
41
+ * delay is variable — worst exactly while scrolling, because that is when the JS
42
+ * thread is busiest. Fading instead of moving does not hide it either: the eye
43
+ * still sees two pieces of chrome change at different moments.
44
+ *
45
+ * A collapse is MOTION, and motion that must stay in lock-step with a
46
+ * reanimated animation has to travel as a shared value on the UI thread. React
47
+ * state is the wrong transport at any width, so it is not offered here at all.
48
+ * The app owns that signal — Mention drives its header, its bar and its FAB from
49
+ * one `SharedValue` through `useAnimatedStyle` — and this registry stays what it
50
+ * is good at: GEOMETRY. Where things are, not when they move.
44
51
  *
45
52
  * ## Why an external store
46
53
  *
@@ -60,46 +67,33 @@ import {
60
67
  type PropsWithChildren,
61
68
  } from 'react';
62
69
 
63
- interface Claim {
64
- /** What the surface keeps permanently free, collapsed or not. */
65
- reserved: number;
66
- /** What it occupies right now. */
67
- current: number;
68
- }
69
-
70
70
  interface BottomEdgeStore {
71
71
  subscribe: (onChange: () => void) => () => void;
72
72
  /**
73
- * The cached totals. Each returns the SAME number until a claim actually
74
- * changes it — `useSyncExternalStore` re-renders forever if the snapshot is
75
- * recomputed per call, and a reader of one channel must not re-render when
76
- * only the other moved.
73
+ * The cached total. Returns the SAME number until a claim actually changes it
74
+ * — `useSyncExternalStore` re-renders forever if the snapshot is recomputed
75
+ * per call.
77
76
  */
78
- getReserved: () => number;
79
- getLive: () => number;
80
- claim: (id: string, reserved: number, current: number) => void;
77
+ getInset: () => number;
78
+ claim: (id: string, height: number) => void;
81
79
  release: (id: string) => void;
82
80
  }
83
81
 
84
82
  function createBottomEdgeStore(): BottomEdgeStore {
85
- const claims = new Map<string, Claim>();
83
+ const claims = new Map<string, number>();
86
84
  const listeners = new Set<() => void>();
87
- let reserved = 0;
88
- let live = 0;
85
+ let inset = 0;
89
86
 
90
87
  const recompute = () => {
91
- let nextReserved = 0;
92
- let nextLive = 0;
93
- for (const claim of claims.values()) {
94
- if (claim.reserved > nextReserved) nextReserved = claim.reserved;
95
- if (claim.current > nextLive) nextLive = claim.current;
88
+ let next = 0;
89
+ for (const height of claims.values()) {
90
+ if (height > next) next = height;
96
91
  }
97
- // Bail before notifying: a re-registration at unchanged heights (every
92
+ // Bail before notifying: a re-registration at an unchanged height (every
98
93
  // render of a claimant whose footprint did not move) must not re-render
99
94
  // every reader.
100
- if (nextReserved === reserved && nextLive === live) return;
101
- reserved = nextReserved;
102
- live = nextLive;
95
+ if (next === inset) return;
96
+ inset = next;
103
97
  for (const listener of listeners) listener();
104
98
  };
105
99
 
@@ -110,12 +104,10 @@ function createBottomEdgeStore(): BottomEdgeStore {
110
104
  listeners.delete(onChange);
111
105
  };
112
106
  },
113
- getReserved: () => reserved,
114
- getLive: () => live,
115
- claim(id, nextReserved, nextCurrent) {
116
- const existing = claims.get(id);
117
- if (existing?.reserved === nextReserved && existing.current === nextCurrent) return;
118
- claims.set(id, { reserved: nextReserved, current: nextCurrent });
107
+ getInset: () => inset,
108
+ claim(id, height) {
109
+ if (claims.get(id) === height) return;
110
+ claims.set(id, height);
119
111
  recompute();
120
112
  },
121
113
  release(id) {
@@ -146,15 +138,13 @@ const NO_SUBSCRIPTION = () => () => {};
146
138
  const NO_INSET = () => 0;
147
139
 
148
140
  /**
149
- * How much of the bottom edge is permanently RESERVED, in px.
141
+ * How much of the bottom edge is already occupied, in px.
150
142
  *
151
- * Never shrinks while its claimant is mounted, so it is the number for anything
152
- * that must not move as the user scrolls — a list's bottom padding, a toast
153
- * stack's offset:
143
+ * Add it to whatever offset the surface would otherwise use:
154
144
  *
155
145
  * ```tsx
156
- * const reserved = useBottomEdgeInset();
157
- * <FlatList contentContainerStyle={{ paddingBottom: reserved + 12 }} />
146
+ * const occupied = useBottomEdgeInset();
147
+ * <View style={{ position: 'absolute', bottom: windowEdgeGap(insets.bottom) + occupied }} />
158
148
  * ```
159
149
  *
160
150
  * `0` outside a provider, and `0` on the first commit even inside one — a claim
@@ -166,54 +156,29 @@ export function useBottomEdgeInset(): number {
166
156
  const store = useContext(BottomEdgeContext);
167
157
  return useSyncExternalStore(
168
158
  store?.subscribe ?? NO_SUBSCRIPTION,
169
- store?.getReserved ?? NO_INSET,
170
- store?.getReserved ?? NO_INSET,
171
- );
172
- }
173
-
174
- /**
175
- * How much of the bottom edge is occupied RIGHT NOW, in px — the same number as
176
- * {@link useBottomEdgeInset} for a surface that does not collapse, and smaller
177
- * while one does.
178
- *
179
- * This is what a surface sitting ON the edge wants, so it rides down with a
180
- * minimizing tab bar rather than leaving a hole above it. It changes when the
181
- * collapse STATE changes, not per frame: a claimant reports its settled
182
- * footprint and the reader animates the difference itself, which keeps the
183
- * motion on whatever animation system that reader already uses instead of
184
- * forcing a shared one through React state.
185
- */
186
- export function useBottomEdgeLiveInset(): number {
187
- const store = useContext(BottomEdgeContext);
188
- return useSyncExternalStore(
189
- store?.subscribe ?? NO_SUBSCRIPTION,
190
- store?.getLive ?? NO_INSET,
191
- store?.getLive ?? NO_INSET,
159
+ store?.getInset ?? NO_INSET,
160
+ store?.getInset ?? NO_INSET,
192
161
  );
193
162
  }
194
163
 
195
164
  /**
196
- * Claim the bottom edge for as long as the caller is mounted.
165
+ * Claim `height` px of the bottom edge for as long as the caller is mounted.
197
166
  *
198
167
  * The claimant owns its own placement — claiming does not move it. It declares
199
- * the space it occupies so that everything reading the edge stays off it. Pass
200
- * the FULL footprint (the surface's height plus the gap it holds off the window
201
- * edge), which is the same number the surface positions itself with.
202
- *
203
- * `current` is what the surface occupies right now; it defaults to `reserved`,
204
- * which is correct for anything that does not collapse. A surface that shrinks
205
- * on scroll passes its live footprint as `current` and keeps `reserved` at its
206
- * full size — see the two channels above for why both are needed.
168
+ * the space it occupies so that everything reading `useBottomEdgeInset()` stays
169
+ * off it. Pass the FULL footprint (the surface's height plus the gap it holds
170
+ * off the window edge), which is the same number the surface positions itself
171
+ * with.
207
172
  *
208
173
  * A no-op outside a provider, so a surface stays usable standalone.
209
174
  */
210
- export function useClaimBottomEdge(reserved: number, current: number = reserved): void {
175
+ export function useClaimBottomEdge(height: number): void {
211
176
  const store = useContext(BottomEdgeContext);
212
177
  const id = useId();
213
178
 
214
179
  useEffect(() => {
215
180
  if (!store) return;
216
- store.claim(id, reserved, current);
181
+ store.claim(id, height);
217
182
  return () => store.release(id);
218
- }, [store, id, reserved, current]);
183
+ }, [store, id, height]);
219
184
  }
@@ -5,9 +5,4 @@
5
5
  * parked there". A surface that floats at the bottom needs both.
6
6
  */
7
7
  export { EDGE_GAP, windowEdgeGap } from './edge';
8
- export {
9
- BottomEdgeProvider,
10
- useBottomEdgeInset,
11
- useBottomEdgeLiveInset,
12
- useClaimBottomEdge,
13
- } from './bottom-edge';
8
+ export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
@@ -20,7 +20,6 @@ import {
20
20
  useContext,
21
21
  useEffect,
22
22
  useMemo,
23
- useState,
24
23
  type ComponentType,
25
24
  } from 'react';
26
25
  import { Pressable, StyleSheet, useWindowDimensions, View, type ViewStyle } from 'react-native';
@@ -30,7 +29,6 @@ import Animated, {
30
29
  interpolate,
31
30
  interpolateColor,
32
31
  runOnJS,
33
- useAnimatedReaction,
34
32
  useAnimatedStyle,
35
33
  useSharedValue,
36
34
  withSpring,
@@ -424,36 +422,13 @@ function TabBarBody({
424
422
  // its own layout can never drift from where the bar actually sits.
425
423
  const bottomOffset = windowEdgeGap(insets.bottom);
426
424
 
427
- // Whether the bar has settled into its minimized size, as REACT state.
428
- //
429
- // Reacting on `target` rather than on `progress` is what keeps this cheap:
430
- // `target` is the binary 0/1 the minimize spring is heading for, so this fires
431
- // twice per scroll gesture instead of once per frame. Crossing `progress > 0.5`
432
- // would work too and would cost the same, but it would report the change
433
- // halfway through the animation rather than when it was decided.
434
- const [isMinimized, setIsMinimized] = useState(false);
435
- useAnimatedReaction(
436
- () => minimized.target.value,
437
- (target, previous) => {
438
- if (target !== previous) runOnJS(setIsMinimized)(target === 1);
439
- },
440
- [minimized],
441
- );
442
-
443
425
  // Publish what the bar occupies so anything else at this edge stacks above it
444
- // rather than behind it. Same numbers `useTabBarFootprint` reports, derived
445
- // from the same geometry, so a consumer reading either can never disagree with
446
- // where the bar actually sits.
447
- //
448
- // RESERVED stays the EXPANDED height: the bar re-expands the moment the user
449
- // scrolls back up, so anything that permanently reserves space (a list's
450
- // bottom padding, a toast) must keep room for the full pill or it would jitter
451
- // on every scroll. LIVE follows the minimize, so a surface sitting ON the edge
452
- // rides down with the bar instead of leaving a 14px hole above it.
453
- useClaimBottomEdge(
454
- bottomOffset + EXPANDED_HEIGHT,
455
- bottomOffset + (isMinimized ? MINIMIZED_HEIGHT : EXPANDED_HEIGHT),
456
- );
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);
457
432
 
458
433
  // How centring and the animated inset compose: centring is STATIC and belongs
459
434
  // to the wrap, the inset stays ANIMATED on the pill inside it. The wrap is