@mrmeg/expo-ui 0.1.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/README.md +96 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.js +149 -0
- package/dist/components/Alert.d.ts +30 -0
- package/dist/components/Alert.js +25 -0
- package/dist/components/AnimatedView.d.ts +55 -0
- package/dist/components/AnimatedView.js +39 -0
- package/dist/components/Badge.d.ts +23 -0
- package/dist/components/Badge.js +74 -0
- package/dist/components/BottomSheet.d.ts +74 -0
- package/dist/components/BottomSheet.js +513 -0
- package/dist/components/Button.d.ts +129 -0
- package/dist/components/Button.js +216 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +126 -0
- package/dist/components/Checkbox.d.ts +39 -0
- package/dist/components/Checkbox.js +96 -0
- package/dist/components/Collapsible.d.ts +67 -0
- package/dist/components/Collapsible.js +38 -0
- package/dist/components/Dialog.d.ts +140 -0
- package/dist/components/Dialog.js +167 -0
- package/dist/components/DismissKeyboard.d.ts +15 -0
- package/dist/components/DismissKeyboard.js +13 -0
- package/dist/components/Drawer.d.ts +74 -0
- package/dist/components/Drawer.js +423 -0
- package/dist/components/DropdownMenu.d.ts +120 -0
- package/dist/components/DropdownMenu.js +211 -0
- package/dist/components/EmptyState.d.ts +42 -0
- package/dist/components/EmptyState.js +58 -0
- package/dist/components/ErrorBoundary.d.ts +53 -0
- package/dist/components/ErrorBoundary.js +75 -0
- package/dist/components/Icon.d.ts +46 -0
- package/dist/components/Icon.js +40 -0
- package/dist/components/InputOTP.d.ts +72 -0
- package/dist/components/InputOTP.js +155 -0
- package/dist/components/Label.d.ts +61 -0
- package/dist/components/Label.js +72 -0
- package/dist/components/MaxWidthContainer.d.ts +58 -0
- package/dist/components/MaxWidthContainer.js +64 -0
- package/dist/components/Notification.d.ts +26 -0
- package/dist/components/Notification.js +230 -0
- package/dist/components/Popover.d.ts +79 -0
- package/dist/components/Popover.js +91 -0
- package/dist/components/Progress.d.ts +28 -0
- package/dist/components/Progress.js +107 -0
- package/dist/components/RadioGroup.d.ts +65 -0
- package/dist/components/RadioGroup.js +142 -0
- package/dist/components/Select.d.ts +88 -0
- package/dist/components/Select.js +172 -0
- package/dist/components/Separator.d.ts +83 -0
- package/dist/components/Separator.js +85 -0
- package/dist/components/Skeleton.d.ts +68 -0
- package/dist/components/Skeleton.js +99 -0
- package/dist/components/Slider.d.ts +24 -0
- package/dist/components/Slider.js +162 -0
- package/dist/components/StatusBar.d.ts +1 -0
- package/dist/components/StatusBar.js +19 -0
- package/dist/components/StyledText.d.ts +161 -0
- package/dist/components/StyledText.js +193 -0
- package/dist/components/Switch.d.ts +44 -0
- package/dist/components/Switch.js +129 -0
- package/dist/components/Tabs.d.ts +31 -0
- package/dist/components/Tabs.js +127 -0
- package/dist/components/TextInput.d.ts +120 -0
- package/dist/components/TextInput.js +263 -0
- package/dist/components/Toggle.d.ts +106 -0
- package/dist/components/Toggle.js +150 -0
- package/dist/components/ToggleGroup.d.ts +80 -0
- package/dist/components/ToggleGroup.js +189 -0
- package/dist/components/Tooltip.d.ts +121 -0
- package/dist/components/Tooltip.js +132 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +35 -0
- package/dist/constants/colors.d.ts +82 -0
- package/dist/constants/colors.js +116 -0
- package/dist/constants/fonts.d.ts +32 -0
- package/dist/constants/fonts.js +91 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.js +3 -0
- package/dist/constants/spacing.d.ts +40 -0
- package/dist/constants/spacing.js +48 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useDimensions.d.ts +19 -0
- package/dist/hooks/useDimensions.js +55 -0
- package/dist/hooks/useReduceMotion.d.ts +5 -0
- package/dist/hooks/useReduceMotion.js +64 -0
- package/dist/hooks/useResources.d.ts +12 -0
- package/dist/hooks/useResources.js +56 -0
- package/dist/hooks/useScalePress.d.ts +57 -0
- package/dist/hooks/useScalePress.js +55 -0
- package/dist/hooks/useStaggeredEntrance.d.ts +67 -0
- package/dist/hooks/useStaggeredEntrance.js +74 -0
- package/dist/hooks/useTheme.d.ts +88 -0
- package/dist/hooks/useTheme.js +328 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/animations.d.ts +1 -0
- package/dist/lib/animations.js +3 -0
- package/dist/lib/haptics.d.ts +3 -0
- package/dist/lib/haptics.js +29 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/sentry.d.ts +16 -0
- package/dist/lib/sentry.js +55 -0
- package/dist/state/globalUIStore.d.ts +30 -0
- package/dist/state/globalUIStore.js +8 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.js +2 -0
- package/dist/state/themeStore.d.ts +6 -0
- package/dist/state/themeStore.js +38 -0
- package/package.json +92 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { createContext, useCallback, useContext, useEffect, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { View, Pressable, Animated, StyleSheet, Platform, Dimensions, PanResponder, ScrollView, } from "react-native";
|
|
4
|
+
import { Portal } from "@rn-primitives/portal";
|
|
5
|
+
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
6
|
+
import { Pressable as SlotPressable } from "@rn-primitives/slot";
|
|
7
|
+
import { useTheme } from "../hooks/useTheme";
|
|
8
|
+
import { spacing } from "../constants/spacing";
|
|
9
|
+
import { shouldUseNativeDriver } from "../lib/animations";
|
|
10
|
+
import { TextColorContext, TextClassContext } from "./StyledText";
|
|
11
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
|
+
/**
|
|
13
|
+
* BottomSheet Component with Sub-components
|
|
14
|
+
*
|
|
15
|
+
* A sliding bottom sheet overlay with snap points, swipe gestures,
|
|
16
|
+
* and compound component pattern matching Drawer.tsx.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <BottomSheet>
|
|
21
|
+
* <BottomSheet.Trigger asChild>
|
|
22
|
+
* <Button>Open Sheet</Button>
|
|
23
|
+
* </BottomSheet.Trigger>
|
|
24
|
+
* <BottomSheet.Content>
|
|
25
|
+
* <BottomSheet.Handle />
|
|
26
|
+
* <BottomSheet.Header>
|
|
27
|
+
* <SansSerifBoldText>Title</SansSerifBoldText>
|
|
28
|
+
* </BottomSheet.Header>
|
|
29
|
+
* <BottomSheet.Body>
|
|
30
|
+
* <SansSerifText>Content here</SansSerifText>
|
|
31
|
+
* </BottomSheet.Body>
|
|
32
|
+
* <BottomSheet.Footer>
|
|
33
|
+
* <Button>Action</Button>
|
|
34
|
+
* </BottomSheet.Footer>
|
|
35
|
+
* </BottomSheet.Content>
|
|
36
|
+
* </BottomSheet>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
// Platform-specific overlay wrapper
|
|
40
|
+
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Context
|
|
43
|
+
// ============================================================================
|
|
44
|
+
const BottomSheetContext = createContext(null);
|
|
45
|
+
function useBottomSheetContext() {
|
|
46
|
+
const context = useContext(BottomSheetContext);
|
|
47
|
+
if (!context) {
|
|
48
|
+
throw new Error("BottomSheet components must be used within a BottomSheet");
|
|
49
|
+
}
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
const DragContext = createContext(null);
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Utility Functions
|
|
55
|
+
// ============================================================================
|
|
56
|
+
function resolveSnapPoints(points) {
|
|
57
|
+
const screenHeight = Dimensions.get("window").height;
|
|
58
|
+
return points.map((p) => {
|
|
59
|
+
if (typeof p === "number")
|
|
60
|
+
return p;
|
|
61
|
+
return (parseFloat(p) / 100) * screenHeight;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function sheetReducer(state, action) {
|
|
65
|
+
switch (action.type) {
|
|
66
|
+
case "OPEN": return true;
|
|
67
|
+
case "CLOSE": return false;
|
|
68
|
+
case "TOGGLE": return !state;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// BottomSheet Root
|
|
73
|
+
// ============================================================================
|
|
74
|
+
function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, snapPoints: rawSnapPoints = ["50%"], closeOnBackdropPress = true, children, }) {
|
|
75
|
+
const [internalOpen, dispatch] = useReducer(sheetReducer, defaultOpen);
|
|
76
|
+
const isControlled = controlledOpen !== undefined;
|
|
77
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
78
|
+
const snapPoints = resolveSnapPoints(rawSnapPoints);
|
|
79
|
+
const toggle = () => {
|
|
80
|
+
if (isControlled) {
|
|
81
|
+
controlledOnOpenChange?.(!controlledOpen);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
dispatch({ type: "TOGGLE" });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const onOpenChange = (newOpen) => {
|
|
88
|
+
if (isControlled) {
|
|
89
|
+
controlledOnOpenChange?.(newOpen);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
dispatch({ type: newOpen ? "OPEN" : "CLOSE" });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const contextValue = {
|
|
96
|
+
open,
|
|
97
|
+
onOpenChange,
|
|
98
|
+
toggle,
|
|
99
|
+
snapPoints,
|
|
100
|
+
currentSnapIndex: 0,
|
|
101
|
+
closeOnBackdropPress,
|
|
102
|
+
};
|
|
103
|
+
return (_jsx(BottomSheetContext.Provider, { value: contextValue, children: children }));
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Trigger
|
|
107
|
+
// ============================================================================
|
|
108
|
+
function BottomSheetTrigger({ asChild, children, style: styleOverride }) {
|
|
109
|
+
const { toggle } = useBottomSheetContext();
|
|
110
|
+
const handlePress = () => toggle();
|
|
111
|
+
if (asChild && React.isValidElement(children)) {
|
|
112
|
+
return React.cloneElement(children, {
|
|
113
|
+
onPress: handlePress,
|
|
114
|
+
style: [
|
|
115
|
+
children.props.style,
|
|
116
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
117
|
+
styleOverride,
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return (_jsx(Pressable, { onPress: handlePress, style: [
|
|
122
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
123
|
+
styleOverride,
|
|
124
|
+
], children: children }));
|
|
125
|
+
}
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Content
|
|
128
|
+
// ============================================================================
|
|
129
|
+
function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, style: styleOverride, children, ...props }) {
|
|
130
|
+
const sheetContext = useBottomSheetContext();
|
|
131
|
+
const { open, onOpenChange, snapPoints, closeOnBackdropPress } = sheetContext;
|
|
132
|
+
const { theme } = useTheme();
|
|
133
|
+
// Highest snap point is the max height
|
|
134
|
+
const maxHeight = Math.max(...snapPoints);
|
|
135
|
+
// With bottom:0 positioning, translateY=0 means visible, translateY=maxHeight means hidden below
|
|
136
|
+
const closedPosition = maxHeight;
|
|
137
|
+
const translateY = useRef(new Animated.Value(open ? 0 : closedPosition)).current;
|
|
138
|
+
const backdropOpacity = useRef(new Animated.Value(open ? 1 : 0)).current;
|
|
139
|
+
const [isVisible, setIsVisible] = useState(open);
|
|
140
|
+
const lastOpenRef = useRef(null);
|
|
141
|
+
const runningAnimationRef = useRef(null);
|
|
142
|
+
const currentHeightRef = useRef(maxHeight);
|
|
143
|
+
// Track which snap we're at
|
|
144
|
+
const currentSnapRef = useRef(snapPoints.length - 1);
|
|
145
|
+
const textColor = theme.colors.foreground;
|
|
146
|
+
// ------------------------------------------------------------------
|
|
147
|
+
// Shared snap/close logic used by both native PanResponder and web drag
|
|
148
|
+
// ------------------------------------------------------------------
|
|
149
|
+
const handleDragRelease = useCallback((dragDistance, velocity) => {
|
|
150
|
+
const visibleHeight = currentHeightRef.current - dragDistance;
|
|
151
|
+
if (velocity > velocityThreshold / 1000 || dragDistance > currentHeightRef.current * 0.4) {
|
|
152
|
+
const lowerSnaps = snapPoints.filter((s) => s < currentHeightRef.current);
|
|
153
|
+
if (lowerSnaps.length > 0 && dragDistance < currentHeightRef.current * 0.4) {
|
|
154
|
+
const nextSnap = lowerSnaps[lowerSnaps.length - 1];
|
|
155
|
+
currentHeightRef.current = nextSnap;
|
|
156
|
+
const targetY = maxHeight - nextSnap;
|
|
157
|
+
Animated.parallel([
|
|
158
|
+
Animated.spring(translateY, {
|
|
159
|
+
toValue: targetY,
|
|
160
|
+
tension: 65,
|
|
161
|
+
friction: 11,
|
|
162
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
163
|
+
}),
|
|
164
|
+
Animated.timing(backdropOpacity, {
|
|
165
|
+
toValue: 1,
|
|
166
|
+
duration: 150,
|
|
167
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
168
|
+
}),
|
|
169
|
+
]).start();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
Animated.parallel([
|
|
173
|
+
Animated.timing(translateY, {
|
|
174
|
+
toValue: closedPosition,
|
|
175
|
+
duration: 200,
|
|
176
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
177
|
+
}),
|
|
178
|
+
Animated.timing(backdropOpacity, {
|
|
179
|
+
toValue: 0,
|
|
180
|
+
duration: 200,
|
|
181
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
182
|
+
}),
|
|
183
|
+
]).start(() => {
|
|
184
|
+
onOpenChange(false);
|
|
185
|
+
setIsVisible(false);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
let nearestSnap = snapPoints[0];
|
|
191
|
+
let minDistance = Infinity;
|
|
192
|
+
for (const snap of snapPoints) {
|
|
193
|
+
const dist = Math.abs(visibleHeight - snap);
|
|
194
|
+
if (dist < minDistance) {
|
|
195
|
+
minDistance = dist;
|
|
196
|
+
nearestSnap = snap;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
currentHeightRef.current = nearestSnap;
|
|
200
|
+
const targetY = maxHeight - nearestSnap;
|
|
201
|
+
Animated.parallel([
|
|
202
|
+
Animated.spring(translateY, {
|
|
203
|
+
toValue: targetY,
|
|
204
|
+
tension: 65,
|
|
205
|
+
friction: 11,
|
|
206
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
207
|
+
}),
|
|
208
|
+
Animated.timing(backdropOpacity, {
|
|
209
|
+
toValue: 1,
|
|
210
|
+
duration: 150,
|
|
211
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
212
|
+
}),
|
|
213
|
+
]).start();
|
|
214
|
+
}
|
|
215
|
+
}, [snapPoints, maxHeight, closedPosition, translateY, backdropOpacity, onOpenChange, velocityThreshold]);
|
|
216
|
+
const handleDragMove = useCallback((dy) => {
|
|
217
|
+
// Base offset: where the sheet sits at the current snap point
|
|
218
|
+
// translateY=0 means top snap (maxHeight visible), higher values = further down
|
|
219
|
+
const baseOffset = maxHeight - currentHeightRef.current;
|
|
220
|
+
const newY = Math.max(baseOffset, baseOffset + dy);
|
|
221
|
+
translateY.setValue(newY);
|
|
222
|
+
// Progress: 1 = fully at current snap, 0 = fully closed
|
|
223
|
+
const dragFromBase = newY - baseOffset;
|
|
224
|
+
const progress = 1 - dragFromBase / currentHeightRef.current;
|
|
225
|
+
backdropOpacity.setValue(Math.max(0, progress));
|
|
226
|
+
}, [translateY, backdropOpacity, maxHeight]);
|
|
227
|
+
// ------------------------------------------------------------------
|
|
228
|
+
// Trigger animation during render if open changed
|
|
229
|
+
// ------------------------------------------------------------------
|
|
230
|
+
if (open !== lastOpenRef.current) {
|
|
231
|
+
const previousOpen = lastOpenRef.current;
|
|
232
|
+
lastOpenRef.current = open;
|
|
233
|
+
if (runningAnimationRef.current) {
|
|
234
|
+
runningAnimationRef.current.stop();
|
|
235
|
+
runningAnimationRef.current = null;
|
|
236
|
+
}
|
|
237
|
+
if (open) {
|
|
238
|
+
if (!isVisible) {
|
|
239
|
+
setIsVisible(true);
|
|
240
|
+
}
|
|
241
|
+
currentSnapRef.current = snapPoints.length - 1;
|
|
242
|
+
currentHeightRef.current = maxHeight;
|
|
243
|
+
if (previousOpen === null) {
|
|
244
|
+
translateY.setValue(closedPosition);
|
|
245
|
+
backdropOpacity.setValue(0);
|
|
246
|
+
}
|
|
247
|
+
const animation = Animated.parallel([
|
|
248
|
+
Animated.spring(translateY, {
|
|
249
|
+
toValue: 0,
|
|
250
|
+
tension: 65,
|
|
251
|
+
friction: 11,
|
|
252
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
253
|
+
}),
|
|
254
|
+
Animated.timing(backdropOpacity, {
|
|
255
|
+
toValue: 1,
|
|
256
|
+
duration: 200,
|
|
257
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
258
|
+
}),
|
|
259
|
+
]);
|
|
260
|
+
runningAnimationRef.current = animation;
|
|
261
|
+
animation.start(({ finished }) => {
|
|
262
|
+
if (finished)
|
|
263
|
+
runningAnimationRef.current = null;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (previousOpen === true) {
|
|
267
|
+
const animation = Animated.parallel([
|
|
268
|
+
Animated.timing(translateY, {
|
|
269
|
+
toValue: closedPosition,
|
|
270
|
+
duration: 200,
|
|
271
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
272
|
+
}),
|
|
273
|
+
Animated.timing(backdropOpacity, {
|
|
274
|
+
toValue: 0,
|
|
275
|
+
duration: 200,
|
|
276
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
277
|
+
}),
|
|
278
|
+
]);
|
|
279
|
+
runningAnimationRef.current = animation;
|
|
280
|
+
animation.start(({ finished }) => {
|
|
281
|
+
runningAnimationRef.current = null;
|
|
282
|
+
if (finished)
|
|
283
|
+
setIsVisible(false);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ------------------------------------------------------------------
|
|
288
|
+
// Native: PanResponder for swipe gestures on the whole sheet
|
|
289
|
+
// ------------------------------------------------------------------
|
|
290
|
+
const panResponder = useRef(Platform.OS !== "web" && swipeEnabled
|
|
291
|
+
? PanResponder.create({
|
|
292
|
+
onStartShouldSetPanResponder: () => false,
|
|
293
|
+
onMoveShouldSetPanResponder: (_evt, gestureState) => {
|
|
294
|
+
const isVertical = Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
|
|
295
|
+
const isSignificant = Math.abs(gestureState.dy) > 10;
|
|
296
|
+
const isDownward = gestureState.dy > 0;
|
|
297
|
+
return isVertical && isSignificant && isDownward;
|
|
298
|
+
},
|
|
299
|
+
onPanResponderMove: (_evt, gestureState) => {
|
|
300
|
+
handleDragMove(gestureState.dy);
|
|
301
|
+
},
|
|
302
|
+
onPanResponderRelease: (_evt, gestureState) => {
|
|
303
|
+
handleDragRelease(Math.max(0, gestureState.dy), gestureState.vy);
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
: null).current;
|
|
307
|
+
// ------------------------------------------------------------------
|
|
308
|
+
// Web: drag context provides callbacks for Handle's pointer events
|
|
309
|
+
// ------------------------------------------------------------------
|
|
310
|
+
const dragContextValue = Platform.OS === "web" && swipeEnabled
|
|
311
|
+
? {
|
|
312
|
+
onDragMove: handleDragMove,
|
|
313
|
+
onDragEnd: (dy, velocity) => {
|
|
314
|
+
handleDragRelease(Math.max(0, dy), velocity);
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
: null;
|
|
318
|
+
const handleBackdropPress = () => {
|
|
319
|
+
if (closeOnBackdropPress) {
|
|
320
|
+
onOpenChange(false);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
if (!isVisible && !open) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const sheetStyle = {
|
|
327
|
+
position: "absolute",
|
|
328
|
+
left: 0,
|
|
329
|
+
right: 0,
|
|
330
|
+
bottom: 0,
|
|
331
|
+
height: maxHeight,
|
|
332
|
+
backgroundColor: theme.colors.card,
|
|
333
|
+
borderTopLeftRadius: spacing.radiusXl,
|
|
334
|
+
borderTopRightRadius: spacing.radiusXl,
|
|
335
|
+
borderTopWidth: 1,
|
|
336
|
+
borderLeftWidth: 1,
|
|
337
|
+
borderRightWidth: 1,
|
|
338
|
+
borderColor: theme.colors.border,
|
|
339
|
+
...(Platform.OS === "web" && { zIndex: 51 }),
|
|
340
|
+
};
|
|
341
|
+
const sheetContent = (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: children }) }));
|
|
342
|
+
const contentElement = (_jsx(Portal, { name: "bottom-sheet-portal", children: _jsx(FullWindowOverlay, { children: _jsx(BottomSheetContext.Provider, { value: sheetContext, children: _jsxs(View, { style: StyleSheet.absoluteFill, children: [_jsx(Animated.View, { style: [
|
|
343
|
+
StyleSheet.absoluteFill,
|
|
344
|
+
{
|
|
345
|
+
backgroundColor: theme.colors.overlay,
|
|
346
|
+
opacity: backdropOpacity,
|
|
347
|
+
},
|
|
348
|
+
Platform.OS === "web" && { zIndex: 50 },
|
|
349
|
+
], children: _jsx(Pressable, { style: StyleSheet.absoluteFill, onPress: handleBackdropPress }) }), _jsx(Animated.View, { style: [
|
|
350
|
+
sheetStyle,
|
|
351
|
+
{ transform: [{ translateY }] },
|
|
352
|
+
styleOverride && typeof styleOverride !== "function"
|
|
353
|
+
? StyleSheet.flatten(styleOverride)
|
|
354
|
+
: undefined,
|
|
355
|
+
], accessibilityViewIsModal: true, ...(Platform.OS === "web" && {
|
|
356
|
+
role: "dialog",
|
|
357
|
+
"aria-modal": true,
|
|
358
|
+
}), ...(panResponder ? panResponder.panHandlers : {}), ...props, children: dragContextValue ? (_jsx(DragContext.Provider, { value: dragContextValue, children: sheetContent })) : (sheetContent) })] }) }) }) }));
|
|
359
|
+
return contentElement;
|
|
360
|
+
}
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// Handle
|
|
363
|
+
// ============================================================================
|
|
364
|
+
function BottomSheetHandle({ style }) {
|
|
365
|
+
const { theme } = useTheme();
|
|
366
|
+
const dragCtx = useContext(DragContext);
|
|
367
|
+
// Web pointer-event drag — attaches move/up listeners on document
|
|
368
|
+
const dragStartY = useRef(0);
|
|
369
|
+
const lastTimestamp = useRef(0);
|
|
370
|
+
const lastDy = useRef(0);
|
|
371
|
+
const isDragging = useRef(false);
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (Platform.OS !== "web" || !dragCtx)
|
|
374
|
+
return;
|
|
375
|
+
const onPointerMove = (e) => {
|
|
376
|
+
if (!isDragging.current)
|
|
377
|
+
return;
|
|
378
|
+
const dy = e.clientY - dragStartY.current;
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
const dt = (now - lastTimestamp.current) / 1000;
|
|
381
|
+
lastTimestamp.current = now;
|
|
382
|
+
lastDy.current = dy;
|
|
383
|
+
dragCtx.onDragMove(dy);
|
|
384
|
+
// Store velocity data on the event for release calculation
|
|
385
|
+
isDragging._lastDt = dt;
|
|
386
|
+
isDragging._lastDy = dy;
|
|
387
|
+
};
|
|
388
|
+
const onPointerUp = (e) => {
|
|
389
|
+
if (!isDragging.current)
|
|
390
|
+
return;
|
|
391
|
+
isDragging.current = false;
|
|
392
|
+
document.body.style.cursor = "";
|
|
393
|
+
document.body.style.userSelect = "";
|
|
394
|
+
const dy = e.clientY - dragStartY.current;
|
|
395
|
+
const dt = isDragging._lastDt || 0.016;
|
|
396
|
+
const prevDy = isDragging._lastDy || 0;
|
|
397
|
+
const velocity = dt > 0 ? (dy - prevDy) / dt / 1000 : 0;
|
|
398
|
+
dragCtx.onDragEnd(dy, velocity);
|
|
399
|
+
};
|
|
400
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
401
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
402
|
+
return () => {
|
|
403
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
404
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
405
|
+
};
|
|
406
|
+
}, [dragCtx]);
|
|
407
|
+
const handlePointerDown = useCallback((e) => {
|
|
408
|
+
if (Platform.OS !== "web" || !dragCtx)
|
|
409
|
+
return;
|
|
410
|
+
isDragging.current = true;
|
|
411
|
+
dragStartY.current = e.nativeEvent?.clientY ?? e.clientY;
|
|
412
|
+
lastTimestamp.current = Date.now();
|
|
413
|
+
lastDy.current = 0;
|
|
414
|
+
document.body.style.cursor = "grabbing";
|
|
415
|
+
document.body.style.userSelect = "none";
|
|
416
|
+
}, [dragCtx]);
|
|
417
|
+
return (_jsx(View, { style: [
|
|
418
|
+
staticStyles.handleContainer,
|
|
419
|
+
Platform.OS === "web" && { cursor: "grab" },
|
|
420
|
+
style,
|
|
421
|
+
], ...(Platform.OS === "web" && dragCtx
|
|
422
|
+
? { onPointerDown: handlePointerDown }
|
|
423
|
+
: {}), children: _jsx(View, { style: [
|
|
424
|
+
staticStyles.handle,
|
|
425
|
+
{ backgroundColor: theme.colors.muted },
|
|
426
|
+
] }) }));
|
|
427
|
+
}
|
|
428
|
+
// ============================================================================
|
|
429
|
+
// Header
|
|
430
|
+
// ============================================================================
|
|
431
|
+
function BottomSheetHeader({ children, style, ...props }) {
|
|
432
|
+
const { theme } = useTheme();
|
|
433
|
+
return (_jsx(View, { style: [
|
|
434
|
+
{
|
|
435
|
+
paddingHorizontal: spacing.md,
|
|
436
|
+
paddingVertical: spacing.md,
|
|
437
|
+
borderBottomWidth: 1,
|
|
438
|
+
borderBottomColor: theme.colors.border,
|
|
439
|
+
},
|
|
440
|
+
style,
|
|
441
|
+
], ...props, children: children }));
|
|
442
|
+
}
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Body
|
|
445
|
+
// ============================================================================
|
|
446
|
+
function BottomSheetBody({ children, style, ...props }) {
|
|
447
|
+
return (_jsx(ScrollView, { style: [{ flex: 1 }, style], contentContainerStyle: {
|
|
448
|
+
paddingHorizontal: spacing.md,
|
|
449
|
+
paddingVertical: spacing.md,
|
|
450
|
+
}, showsVerticalScrollIndicator: false, ...props, children: children }));
|
|
451
|
+
}
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// Footer
|
|
454
|
+
// ============================================================================
|
|
455
|
+
function BottomSheetFooter({ children, style, ...props }) {
|
|
456
|
+
const { theme } = useTheme();
|
|
457
|
+
const insets = useSafeAreaInsets();
|
|
458
|
+
return (_jsx(View, { style: [
|
|
459
|
+
{
|
|
460
|
+
paddingHorizontal: spacing.md,
|
|
461
|
+
paddingTop: spacing.md,
|
|
462
|
+
paddingBottom: spacing.md + insets.bottom,
|
|
463
|
+
borderTopWidth: 1,
|
|
464
|
+
borderTopColor: theme.colors.border,
|
|
465
|
+
},
|
|
466
|
+
style,
|
|
467
|
+
], ...props, children: children }));
|
|
468
|
+
}
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Close
|
|
471
|
+
// ============================================================================
|
|
472
|
+
function BottomSheetClose({ asChild, children, style: styleOverride }) {
|
|
473
|
+
const { onOpenChange } = useBottomSheetContext();
|
|
474
|
+
const handlePress = () => onOpenChange(false);
|
|
475
|
+
if (asChild) {
|
|
476
|
+
return (_jsx(SlotPressable, { onPress: handlePress, style: [
|
|
477
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
478
|
+
styleOverride,
|
|
479
|
+
], children: children }));
|
|
480
|
+
}
|
|
481
|
+
return (_jsx(Pressable, { onPress: handlePress, style: [
|
|
482
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
483
|
+
styleOverride,
|
|
484
|
+
], children: children }));
|
|
485
|
+
}
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// Static styles
|
|
488
|
+
// ============================================================================
|
|
489
|
+
const staticStyles = StyleSheet.create({
|
|
490
|
+
handleContainer: {
|
|
491
|
+
alignItems: "center",
|
|
492
|
+
paddingTop: spacing.sm,
|
|
493
|
+
paddingBottom: spacing.xs,
|
|
494
|
+
},
|
|
495
|
+
handle: {
|
|
496
|
+
width: 36,
|
|
497
|
+
height: 4,
|
|
498
|
+
borderRadius: 2,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// Compound export
|
|
503
|
+
// ============================================================================
|
|
504
|
+
const BottomSheet = Object.assign(BottomSheetRoot, {
|
|
505
|
+
Trigger: BottomSheetTrigger,
|
|
506
|
+
Content: BottomSheetContent,
|
|
507
|
+
Handle: BottomSheetHandle,
|
|
508
|
+
Header: BottomSheetHeader,
|
|
509
|
+
Body: BottomSheetBody,
|
|
510
|
+
Footer: BottomSheetFooter,
|
|
511
|
+
Close: BottomSheetClose,
|
|
512
|
+
});
|
|
513
|
+
export { BottomSheet, BottomSheetTrigger, BottomSheetContent, BottomSheetHandle, BottomSheetHeader, BottomSheetBody, BottomSheetFooter, BottomSheetClose, useBottomSheetContext, };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { ComponentType } from "react";
|
|
2
|
+
import { PressableProps, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle, ImageStyle } from "react-native";
|
|
3
|
+
import { TextProps } from "./StyledText";
|
|
4
|
+
/**
|
|
5
|
+
* Button variants
|
|
6
|
+
*/
|
|
7
|
+
type Presets = "default" | "outline" | "ghost" | "link" | "destructive" | "secondary";
|
|
8
|
+
/**
|
|
9
|
+
* Button size variants
|
|
10
|
+
*/
|
|
11
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
12
|
+
export interface ButtonAccessoryProps {
|
|
13
|
+
style: StyleProp<ViewStyle | TextStyle | ImageStyle>;
|
|
14
|
+
pressableState: PressableStateCallbackType;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ButtonProps extends PressableProps {
|
|
18
|
+
/**
|
|
19
|
+
* Text which is looked up via i18n.
|
|
20
|
+
*/
|
|
21
|
+
tx?: TextProps["tx"];
|
|
22
|
+
/**
|
|
23
|
+
* The text to display if not using `tx` or nested components.
|
|
24
|
+
*/
|
|
25
|
+
text?: TextProps["text"];
|
|
26
|
+
/**
|
|
27
|
+
* Optional options to pass to i18n.
|
|
28
|
+
*/
|
|
29
|
+
txOptions?: TextProps["txOptions"];
|
|
30
|
+
/**
|
|
31
|
+
* An optional style override useful for padding & margin.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
/**
|
|
35
|
+
* An optional style override for the "pressed" state.
|
|
36
|
+
*/
|
|
37
|
+
pressedStyle?: StyleProp<ViewStyle>;
|
|
38
|
+
/**
|
|
39
|
+
* An optional style override for the button text.
|
|
40
|
+
*/
|
|
41
|
+
textStyle?: StyleProp<TextStyle>;
|
|
42
|
+
/**
|
|
43
|
+
* An optional style override for the button text when in the "pressed" state.
|
|
44
|
+
*/
|
|
45
|
+
pressedTextStyle?: StyleProp<TextStyle>;
|
|
46
|
+
/**
|
|
47
|
+
* An optional style override for the button text when in the "disabled" state.
|
|
48
|
+
*/
|
|
49
|
+
disabledTextStyle?: StyleProp<TextStyle>;
|
|
50
|
+
/**
|
|
51
|
+
* One of the different types of button presets.
|
|
52
|
+
* @default "default"
|
|
53
|
+
*/
|
|
54
|
+
preset?: Presets;
|
|
55
|
+
/**
|
|
56
|
+
* Size variant
|
|
57
|
+
* @default "md"
|
|
58
|
+
*/
|
|
59
|
+
size?: ButtonSize;
|
|
60
|
+
/**
|
|
61
|
+
* An optional component to render on the right side of the text.
|
|
62
|
+
*/
|
|
63
|
+
RightAccessory?: ComponentType<ButtonAccessoryProps>;
|
|
64
|
+
/**
|
|
65
|
+
* An optional component to render on the left side of the text.
|
|
66
|
+
*/
|
|
67
|
+
LeftAccessory?: ComponentType<ButtonAccessoryProps>;
|
|
68
|
+
/**
|
|
69
|
+
* Children components.
|
|
70
|
+
*/
|
|
71
|
+
children?: React.ReactNode;
|
|
72
|
+
/**
|
|
73
|
+
* disabled prop, accessed directly for declarative styling reasons.
|
|
74
|
+
*/
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* An optional style override for the disabled state
|
|
78
|
+
*/
|
|
79
|
+
disabledStyle?: StyleProp<ViewStyle>;
|
|
80
|
+
/**
|
|
81
|
+
* Whether to show shadow
|
|
82
|
+
*/
|
|
83
|
+
withShadow?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether button is loading (shows spinner)
|
|
86
|
+
*/
|
|
87
|
+
loading?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether button should take full width of container
|
|
90
|
+
*/
|
|
91
|
+
fullWidth?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enhanced Button Component
|
|
95
|
+
*
|
|
96
|
+
* Features:
|
|
97
|
+
* - 6 variants (default, outline, ghost, link, destructive, secondary)
|
|
98
|
+
* - 3 sizes (sm, md, lg)
|
|
99
|
+
* - Loading state with spinner
|
|
100
|
+
* - Full width option
|
|
101
|
+
* - Shadow support
|
|
102
|
+
* - Automatic text contrast calculation
|
|
103
|
+
* - Accessory components (left/right)
|
|
104
|
+
*
|
|
105
|
+
* Usage:
|
|
106
|
+
* ```tsx
|
|
107
|
+
* // Default button
|
|
108
|
+
* <Button onPress={handler}>
|
|
109
|
+
* <SansSerifBoldText>Click Me</SansSerifBoldText>
|
|
110
|
+
* </Button>
|
|
111
|
+
*
|
|
112
|
+
* // Different variants
|
|
113
|
+
* <Button preset="outline" onPress={handler}>Outline</Button>
|
|
114
|
+
* <Button preset="ghost" onPress={handler}>Ghost</Button>
|
|
115
|
+
* <Button preset="destructive" onPress={handler}>Delete</Button>
|
|
116
|
+
*
|
|
117
|
+
* // Different sizes
|
|
118
|
+
* <Button size="sm" onPress={handler}>Small</Button>
|
|
119
|
+
* <Button size="lg" onPress={handler}>Large</Button>
|
|
120
|
+
*
|
|
121
|
+
* // Loading state
|
|
122
|
+
* <Button loading onPress={handler}>Processing...</Button>
|
|
123
|
+
*
|
|
124
|
+
* // Full width
|
|
125
|
+
* <Button fullWidth onPress={handler}>Submit</Button>
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare function Button(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
129
|
+
export {};
|