@momo-kits/foundation 0.169.1 → 0.169.2
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/Application/BottomSheet.hooks.ts +17 -37
- package/Application/BottomSheet.parts.tsx +81 -165
- package/Application/BottomSheet.styles.ts +7 -23
- package/Application/BottomSheet.tsx +15 -67
- package/Application/BottomTab/ActiveIndicator.tsx +90 -0
- package/Application/BottomTab/BottomTabBar.styles.ts +48 -43
- package/Application/BottomTab/BottomTabBar.tsx +229 -223
- package/Application/BottomTab/BottomTabInset.tsx +14 -0
- package/Application/BottomTab/CurvedContainer.tsx +153 -0
- package/Application/BottomTab/CustomBottomTabItem.tsx +132 -108
- package/Application/BottomTab/DuoBarCompression.tsx +4 -12
- package/Application/BottomTab/DuoRailTabBar.tsx +174 -96
- package/Application/BottomTab/DuoTabBar.styles.ts +42 -84
- package/Application/BottomTab/DuoTabBar.tsx +35 -115
- package/Application/BottomTab/TabBarIcon.tsx +160 -72
- package/Application/BottomTab/TabScreen.tsx +48 -61
- package/Application/BottomTab/index.tsx +85 -125
- package/Application/BottomTab/metrics.ts +164 -0
- package/Application/BottomTab/tabItems.ts +46 -0
- package/Application/BottomTab/usePressLift.ts +32 -0
- package/Application/ModalIconButton.tsx +66 -0
- package/Application/ModalScreen.tsx +32 -15
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/index.ts +10 -0
- package/Application/modalLayout.ts +177 -0
- package/Application/types.tabs.ts +5 -0
- package/Application/utils.ts +0 -36
- package/Layout/ReservedRegions.tsx +21 -4
- package/Layout/Screen.tsx +11 -2
- package/Layout/index.ts +2 -0
- package/Popup/PopupNotify.tsx +38 -68
- package/Popup/PopupPromotion.tsx +5 -4
- package/Popup/utils.ts +1 -26
- package/package.json +2 -1
- package/Application/BottomTab/DuoTabBar.parts.tsx +0 -202
- package/Application/BottomTab/badge.ts +0 -41
- package/Application/BottomTab/hooks.ts +0 -358
- package/Application/BottomTab/index.styles.ts +0 -30
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
-
import {
|
|
4
|
-
clamp,
|
|
5
|
-
useAnimatedReaction,
|
|
6
|
-
useAnimatedStyle,
|
|
7
|
-
useReducedMotion,
|
|
8
|
-
useSharedValue,
|
|
9
|
-
withSequence,
|
|
10
|
-
withSpring,
|
|
11
|
-
withTiming,
|
|
12
|
-
} from 'react-native-reanimated';
|
|
13
|
-
import { scheduleOnRN } from 'react-native-worklets';
|
|
14
|
-
import { ApplicationContext } from '../../Context';
|
|
15
|
-
|
|
16
|
-
type Axis = 'x' | 'y';
|
|
17
|
-
|
|
18
|
-
type Options = {
|
|
19
|
-
count: number;
|
|
20
|
-
// distance between two neighbouring tab centers
|
|
21
|
-
pitch: number;
|
|
22
|
-
// position of the first tab's center inside the gesture view
|
|
23
|
-
firstCenter: number;
|
|
24
|
-
axis: Axis;
|
|
25
|
-
index: number;
|
|
26
|
-
onSelect: (index: number) => void;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const SPRING = { damping: 18, stiffness: 220, mass: 0.8 };
|
|
30
|
-
const DRAG_SPRING = { damping: 24, stiffness: 600, mass: 0.6 };
|
|
31
|
-
const REDUCED_TIMING = { duration: 200 };
|
|
32
|
-
const LENS_SCALE_X = 1.2;
|
|
33
|
-
const LENS_SCALE_Y = 1.3;
|
|
34
|
-
const BUTTON_LIFT_RATIO = 0.5;
|
|
35
|
-
const STRETCH_PER_POINT = 0.012;
|
|
36
|
-
const MAX_STRETCH = 0.25;
|
|
37
|
-
const STRETCH_EASING = 0.25;
|
|
38
|
-
const REST_STRETCH = 0.002;
|
|
39
|
-
const CROSS_STRETCH_RATIO = 0.5;
|
|
40
|
-
const TAP_LIFT_DURATION = 120;
|
|
41
|
-
const BAR_SWELL = 0.04;
|
|
42
|
-
const ICON_POP = 1.15;
|
|
43
|
-
const ICON_POP_DURATION = 120;
|
|
44
|
-
const DRAG_SLOP = 10;
|
|
45
|
-
// UIScrollView rubber band constant
|
|
46
|
-
const RUBBER_BAND = 0.55;
|
|
47
|
-
const RESYNC_DELAY = 100;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Rubber band past the first and last tab: the further the finger goes, the
|
|
51
|
-
* less the lens follows.
|
|
52
|
-
*/
|
|
53
|
-
const rubberBand = (value: number, max: number, dimension: number) => {
|
|
54
|
-
'worklet';
|
|
55
|
-
const band = (overshoot: number) => {
|
|
56
|
-
return (1 - 1 / ((overshoot * RUBBER_BAND) / dimension + 1)) * dimension;
|
|
57
|
-
};
|
|
58
|
-
if (value < 0) {
|
|
59
|
-
return -band(-value);
|
|
60
|
-
}
|
|
61
|
-
if (value > max) {
|
|
62
|
-
return max + band(value - max);
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* iOS 26 tab bar selection motion without the glass: press lifts a lens over
|
|
69
|
-
* the tab, a drag slides it under the finger, release selects the tab under it.
|
|
70
|
-
*/
|
|
71
|
-
const useLiquidSelection = ({
|
|
72
|
-
count,
|
|
73
|
-
pitch,
|
|
74
|
-
firstCenter,
|
|
75
|
-
axis,
|
|
76
|
-
index,
|
|
77
|
-
onSelect,
|
|
78
|
-
}: Options) => {
|
|
79
|
-
const { navigator } = useContext<any>(ApplicationContext);
|
|
80
|
-
const reduceMotion = useReducedMotion();
|
|
81
|
-
|
|
82
|
-
const offset = useSharedValue(index * pitch);
|
|
83
|
-
// lets a re-render skip a spring already heading to the same target
|
|
84
|
-
const destination = useSharedValue(index * pitch);
|
|
85
|
-
const selected = useSharedValue(index);
|
|
86
|
-
const hovered = useSharedValue(index);
|
|
87
|
-
const lift = useSharedValue(0);
|
|
88
|
-
const isDragging = useSharedValue(false);
|
|
89
|
-
const stretch = useSharedValue(0);
|
|
90
|
-
const metrics = useSharedValue({ count, pitch, firstCenter });
|
|
91
|
-
const hasMeasured = useRef(pitch > 0);
|
|
92
|
-
|
|
93
|
-
const indexRef = useRef(index);
|
|
94
|
-
const pitchRef = useRef(pitch);
|
|
95
|
-
// read through refs so the gesture is never rebuilt mid drag
|
|
96
|
-
const callbacks = useRef({ onSelect, navigator });
|
|
97
|
-
indexRef.current = index;
|
|
98
|
-
pitchRef.current = pitch;
|
|
99
|
-
callbacks.current = { onSelect, navigator };
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
metrics.value = { count, pitch, firstCenter };
|
|
103
|
-
}, [count, pitch, firstCenter, metrics]);
|
|
104
|
-
|
|
105
|
-
const move = useCallback(
|
|
106
|
-
(target: number, velocity: number, config: typeof SPRING) => {
|
|
107
|
-
'worklet';
|
|
108
|
-
if (destination.value === target && velocity === 0) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
destination.value = target;
|
|
112
|
-
if (reduceMotion) {
|
|
113
|
-
offset.value = withTiming(target, REDUCED_TIMING);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
offset.value = withSpring(target, { ...config, velocity });
|
|
117
|
-
},
|
|
118
|
-
[offset, destination, reduceMotion],
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
const target = index * pitch;
|
|
123
|
-
selected.value = index;
|
|
124
|
-
// snap on first layout so the lens doesn't slide in from the leading edge
|
|
125
|
-
if (hasMeasured.current) {
|
|
126
|
-
move(target, 0, SPRING);
|
|
127
|
-
} else {
|
|
128
|
-
destination.value = target;
|
|
129
|
-
offset.value = target;
|
|
130
|
-
}
|
|
131
|
-
hasMeasured.current = pitch > 0;
|
|
132
|
-
}, [index, pitch, offset, destination, selected, move]);
|
|
133
|
-
|
|
134
|
-
useAnimatedReaction(
|
|
135
|
-
() => offset.value,
|
|
136
|
-
(current, previous) => {
|
|
137
|
-
if (previous === null || reduceMotion) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const speed = Math.abs(current - previous);
|
|
141
|
-
const target = Math.min(speed * STRETCH_PER_POINT, MAX_STRETCH);
|
|
142
|
-
// eased both ways so a slow frame never snaps the shape
|
|
143
|
-
const next = stretch.value + (target - stretch.value) * STRETCH_EASING;
|
|
144
|
-
stretch.value = next < REST_STRETCH ? 0 : next;
|
|
145
|
-
},
|
|
146
|
-
[reduceMotion],
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Returns the lens to the selected tab, unless the tab it is heading to got
|
|
151
|
-
* selected in the meantime.
|
|
152
|
-
*/
|
|
153
|
-
const resync = useCallback(() => {
|
|
154
|
-
setTimeout(() => {
|
|
155
|
-
const target = indexRef.current * pitchRef.current;
|
|
156
|
-
if (!isDragging.value && destination.value !== target) {
|
|
157
|
-
move(target, 0, SPRING);
|
|
158
|
-
}
|
|
159
|
-
}, RESYNC_DELAY);
|
|
160
|
-
}, [destination, isDragging, move]);
|
|
161
|
-
|
|
162
|
-
const handlePressIn = (itemIndex: number) => {
|
|
163
|
-
move(itemIndex * pitch, 0, SPRING);
|
|
164
|
-
if (!reduceMotion) {
|
|
165
|
-
lift.value = withSpring(1, SPRING);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Settles the lift after a tap. Returns false when a drag took the press over.
|
|
171
|
-
*/
|
|
172
|
-
const handlePressOut = () => {
|
|
173
|
-
if (isDragging.value) {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
if (!reduceMotion) {
|
|
177
|
-
lift.value = withSequence(
|
|
178
|
-
withTiming(1, { duration: TAP_LIFT_DURATION }),
|
|
179
|
-
withSpring(0, SPRING),
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
// onPress switches to the pressed tab, resync covers a cancelled press
|
|
183
|
-
resync();
|
|
184
|
-
return true;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const tick = useCallback(() => {
|
|
188
|
-
callbacks.current.navigator?.maxApi?.triggerEventVibration?.('light');
|
|
189
|
-
}, []);
|
|
190
|
-
|
|
191
|
-
const select = useCallback(
|
|
192
|
-
(target: number) => {
|
|
193
|
-
callbacks.current.onSelect(target);
|
|
194
|
-
resync();
|
|
195
|
-
},
|
|
196
|
-
[resync],
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const gesture = useMemo(() => {
|
|
200
|
-
const position = (e: { x: number; y: number }) => {
|
|
201
|
-
'worklet';
|
|
202
|
-
return (axis === 'x' ? e.x : e.y) - metrics.value.firstCenter;
|
|
203
|
-
};
|
|
204
|
-
const nearest = (value: number) => {
|
|
205
|
-
'worklet';
|
|
206
|
-
const { count: tabs, pitch: step } = metrics.value;
|
|
207
|
-
return step > 0 ? clamp(Math.round(value / step), 0, tabs - 1) : 0;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
return Gesture.Pan()
|
|
211
|
-
.minDistance(DRAG_SLOP)
|
|
212
|
-
.shouldCancelWhenOutside(false)
|
|
213
|
-
.onStart(e => {
|
|
214
|
-
isDragging.value = true;
|
|
215
|
-
hovered.value = nearest(position(e));
|
|
216
|
-
if (!reduceMotion) {
|
|
217
|
-
lift.value = withSpring(1, SPRING);
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
.onUpdate(e => {
|
|
221
|
-
const { count: tabs, pitch: step } = metrics.value;
|
|
222
|
-
const value = position(e);
|
|
223
|
-
const target = nearest(value);
|
|
224
|
-
if (target !== hovered.value) {
|
|
225
|
-
hovered.value = target;
|
|
226
|
-
scheduleOnRN(tick);
|
|
227
|
-
}
|
|
228
|
-
const follow = rubberBand(value, (tabs - 1) * step, step);
|
|
229
|
-
if (reduceMotion) {
|
|
230
|
-
destination.value = follow;
|
|
231
|
-
offset.value = follow;
|
|
232
|
-
} else {
|
|
233
|
-
move(follow, 0, DRAG_SPRING);
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
.onEnd((e, success) => {
|
|
237
|
-
isDragging.value = false;
|
|
238
|
-
lift.value = reduceMotion ? 0 : withSpring(0, SPRING);
|
|
239
|
-
const raw = axis === 'x' ? e.velocityX : e.velocityY;
|
|
240
|
-
const velocity = Number.isFinite(raw) ? raw : 0;
|
|
241
|
-
if (!success) {
|
|
242
|
-
move(selected.value * metrics.value.pitch, velocity, SPRING);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const target = nearest(position(e));
|
|
246
|
-
move(target * metrics.value.pitch, velocity, SPRING);
|
|
247
|
-
scheduleOnRN(select, target);
|
|
248
|
-
});
|
|
249
|
-
}, [
|
|
250
|
-
axis,
|
|
251
|
-
reduceMotion,
|
|
252
|
-
offset,
|
|
253
|
-
destination,
|
|
254
|
-
selected,
|
|
255
|
-
hovered,
|
|
256
|
-
lift,
|
|
257
|
-
isDragging,
|
|
258
|
-
metrics,
|
|
259
|
-
move,
|
|
260
|
-
tick,
|
|
261
|
-
select,
|
|
262
|
-
]);
|
|
263
|
-
|
|
264
|
-
const bubbleStyle = useAnimatedStyle(() => {
|
|
265
|
-
const isHorizontal = axis === 'x';
|
|
266
|
-
const along = 1 + stretch.value;
|
|
267
|
-
const across = 1 - stretch.value * CROSS_STRETCH_RATIO;
|
|
268
|
-
const lensX = 1 + lift.value * (LENS_SCALE_X - 1);
|
|
269
|
-
const lensY = 1 + lift.value * (LENS_SCALE_Y - 1);
|
|
270
|
-
const scaleX = lensX * (isHorizontal ? along : across);
|
|
271
|
-
const scaleY = lensY * (isHorizontal ? across : along);
|
|
272
|
-
const translateX = isHorizontal ? offset.value : 0;
|
|
273
|
-
const translateY = isHorizontal ? 0 : offset.value;
|
|
274
|
-
return {
|
|
275
|
-
transform: [{ translateX }, { translateY }, { scaleX }, { scaleY }],
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const barStyle = useAnimatedStyle(() => ({
|
|
280
|
-
transform: [{ scale: 1 + lift.value * BAR_SWELL }],
|
|
281
|
-
}));
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
gesture,
|
|
285
|
-
bubbleStyle,
|
|
286
|
-
barStyle,
|
|
287
|
-
handlePressIn,
|
|
288
|
-
handlePressOut,
|
|
289
|
-
offset,
|
|
290
|
-
selected,
|
|
291
|
-
pitch,
|
|
292
|
-
reduceMotion,
|
|
293
|
-
};
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
type LiquidSelection = ReturnType<typeof useLiquidSelection>;
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Per tab styles: the active tint follows the lens over the tab, and the icon
|
|
300
|
-
* pops when the lens lands on it.
|
|
301
|
-
*/
|
|
302
|
-
const useLiquidItem = (
|
|
303
|
-
{ offset, selected, pitch, reduceMotion }: LiquidSelection,
|
|
304
|
-
itemIndex: number,
|
|
305
|
-
) => {
|
|
306
|
-
const pop = useSharedValue(1);
|
|
307
|
-
|
|
308
|
-
useAnimatedReaction(
|
|
309
|
-
() => selected.value === itemIndex,
|
|
310
|
-
(isSelected, wasSelected) => {
|
|
311
|
-
if (isSelected && wasSelected === false && !reduceMotion) {
|
|
312
|
-
pop.value = withSequence(
|
|
313
|
-
withTiming(ICON_POP, { duration: ICON_POP_DURATION }),
|
|
314
|
-
withSpring(1, SPRING),
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
[itemIndex, reduceMotion],
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const activeStyle = useAnimatedStyle(() => {
|
|
322
|
-
if (pitch <= 0) {
|
|
323
|
-
return { opacity: selected.value === itemIndex ? 1 : 0 };
|
|
324
|
-
}
|
|
325
|
-
const distance = Math.abs(offset.value - itemIndex * pitch) / pitch;
|
|
326
|
-
return { opacity: clamp(1 - distance, 0, 1) };
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const popStyle = useAnimatedStyle(() => ({
|
|
330
|
-
transform: [{ scale: pop.value }],
|
|
331
|
-
}));
|
|
332
|
-
|
|
333
|
-
return { activeStyle, popStyle };
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Press lift for a single button, e.g. the compressed rail.
|
|
338
|
-
*/
|
|
339
|
-
const usePressLift = () => {
|
|
340
|
-
const reduceMotion = useReducedMotion();
|
|
341
|
-
const lift = useSharedValue(0);
|
|
342
|
-
const style = useAnimatedStyle(() => {
|
|
343
|
-
const scale = 1 + lift.value * (LENS_SCALE_X - 1) * BUTTON_LIFT_RATIO;
|
|
344
|
-
return { transform: [{ scale }] };
|
|
345
|
-
});
|
|
346
|
-
const onPressIn = () => {
|
|
347
|
-
if (!reduceMotion) {
|
|
348
|
-
lift.value = withSpring(1, SPRING);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
const onPressOut = () => {
|
|
352
|
-
lift.value = withSpring(0, SPRING);
|
|
353
|
-
};
|
|
354
|
-
return { style, onPressIn, onPressOut };
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
export type { LiquidSelection };
|
|
358
|
-
export { useLiquidSelection, useLiquidItem, usePressLift };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
2
|
-
import { Colors } from '../../Consts';
|
|
3
|
-
|
|
4
|
-
export const styles = StyleSheet.create({
|
|
5
|
-
container: { flex: 1 },
|
|
6
|
-
indicatorContainer: { position: 'absolute', top: 0 },
|
|
7
|
-
indicator: {
|
|
8
|
-
height: 2,
|
|
9
|
-
width: 48,
|
|
10
|
-
alignSelf: 'center',
|
|
11
|
-
borderBottomLeftRadius: 2,
|
|
12
|
-
borderBottomRightRadius: 2,
|
|
13
|
-
},
|
|
14
|
-
tabBarStyle: {
|
|
15
|
-
...Platform.select({
|
|
16
|
-
ios: {
|
|
17
|
-
shadowColor: Colors.black_10,
|
|
18
|
-
shadowOffset: { width: 0, height: 4 },
|
|
19
|
-
shadowOpacity: 1,
|
|
20
|
-
shadowRadius: 4,
|
|
21
|
-
},
|
|
22
|
-
android: {
|
|
23
|
-
elevation: 4,
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
tabStyle: {
|
|
28
|
-
paddingVertical: 4,
|
|
29
|
-
},
|
|
30
|
-
});
|