@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.
- package/docs/layout.mdx +20 -29
- package/lib/commonjs/fab/Fab.js +0 -42
- package/lib/commonjs/fab/Fab.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +3 -18
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/layout/bottom-edge.js +42 -68
- package/lib/commonjs/layout/bottom-edge.js.map +1 -1
- package/lib/commonjs/layout/index.js +0 -6
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +6 -22
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/fab/Fab.js +2 -44
- package/lib/module/fab/Fab.js.map +1 -1
- package/lib/module/fab/Fab.web.js +4 -19
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/layout/bottom-edge.js +42 -67
- package/lib/module/layout/bottom-edge.js.map +1 -1
- package/lib/module/layout/index.js +1 -1
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +8 -24
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts +28 -40
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/index.d.ts +1 -1
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-edge.d.ts +28 -40
- package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/module/layout/index.d.ts +1 -1
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/fab/Fab.tsx +3 -41
- package/src/fab/Fab.web.tsx +4 -19
- package/src/layout/bottom-edge.tsx +49 -84
- package/src/layout/index.ts +1 -6
- 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
|
-
* ##
|
|
30
|
+
* ## What does NOT belong here: the collapse
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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
|
-
*
|
|
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
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
-
|
|
79
|
-
|
|
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,
|
|
83
|
+
const claims = new Map<string, number>();
|
|
86
84
|
const listeners = new Set<() => void>();
|
|
87
|
-
let
|
|
88
|
-
let live = 0;
|
|
85
|
+
let inset = 0;
|
|
89
86
|
|
|
90
87
|
const recompute = () => {
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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 (
|
|
101
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
141
|
+
* How much of the bottom edge is already occupied, in px.
|
|
150
142
|
*
|
|
151
|
-
*
|
|
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
|
|
157
|
-
* <
|
|
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?.
|
|
170
|
-
store?.
|
|
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
|
|
200
|
-
* the FULL footprint (the surface's height plus the gap it holds
|
|
201
|
-
* edge), which is the same number the surface positions itself
|
|
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(
|
|
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,
|
|
181
|
+
store.claim(id, height);
|
|
217
182
|
return () => store.release(id);
|
|
218
|
-
}, [store, id,
|
|
183
|
+
}, [store, id, height]);
|
|
219
184
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -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
|
|
445
|
-
//
|
|
446
|
-
// where the bar actually sits.
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
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
|