@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.
Files changed (112) hide show
  1. package/README.md +96 -0
  2. package/dist/components/Accordion.d.ts +54 -0
  3. package/dist/components/Accordion.js +149 -0
  4. package/dist/components/Alert.d.ts +30 -0
  5. package/dist/components/Alert.js +25 -0
  6. package/dist/components/AnimatedView.d.ts +55 -0
  7. package/dist/components/AnimatedView.js +39 -0
  8. package/dist/components/Badge.d.ts +23 -0
  9. package/dist/components/Badge.js +74 -0
  10. package/dist/components/BottomSheet.d.ts +74 -0
  11. package/dist/components/BottomSheet.js +513 -0
  12. package/dist/components/Button.d.ts +129 -0
  13. package/dist/components/Button.js +216 -0
  14. package/dist/components/Card.d.ts +42 -0
  15. package/dist/components/Card.js +126 -0
  16. package/dist/components/Checkbox.d.ts +39 -0
  17. package/dist/components/Checkbox.js +96 -0
  18. package/dist/components/Collapsible.d.ts +67 -0
  19. package/dist/components/Collapsible.js +38 -0
  20. package/dist/components/Dialog.d.ts +140 -0
  21. package/dist/components/Dialog.js +167 -0
  22. package/dist/components/DismissKeyboard.d.ts +15 -0
  23. package/dist/components/DismissKeyboard.js +13 -0
  24. package/dist/components/Drawer.d.ts +74 -0
  25. package/dist/components/Drawer.js +423 -0
  26. package/dist/components/DropdownMenu.d.ts +120 -0
  27. package/dist/components/DropdownMenu.js +211 -0
  28. package/dist/components/EmptyState.d.ts +42 -0
  29. package/dist/components/EmptyState.js +58 -0
  30. package/dist/components/ErrorBoundary.d.ts +53 -0
  31. package/dist/components/ErrorBoundary.js +75 -0
  32. package/dist/components/Icon.d.ts +46 -0
  33. package/dist/components/Icon.js +40 -0
  34. package/dist/components/InputOTP.d.ts +72 -0
  35. package/dist/components/InputOTP.js +155 -0
  36. package/dist/components/Label.d.ts +61 -0
  37. package/dist/components/Label.js +72 -0
  38. package/dist/components/MaxWidthContainer.d.ts +58 -0
  39. package/dist/components/MaxWidthContainer.js +64 -0
  40. package/dist/components/Notification.d.ts +26 -0
  41. package/dist/components/Notification.js +230 -0
  42. package/dist/components/Popover.d.ts +79 -0
  43. package/dist/components/Popover.js +91 -0
  44. package/dist/components/Progress.d.ts +28 -0
  45. package/dist/components/Progress.js +107 -0
  46. package/dist/components/RadioGroup.d.ts +65 -0
  47. package/dist/components/RadioGroup.js +142 -0
  48. package/dist/components/Select.d.ts +88 -0
  49. package/dist/components/Select.js +172 -0
  50. package/dist/components/Separator.d.ts +83 -0
  51. package/dist/components/Separator.js +85 -0
  52. package/dist/components/Skeleton.d.ts +68 -0
  53. package/dist/components/Skeleton.js +99 -0
  54. package/dist/components/Slider.d.ts +24 -0
  55. package/dist/components/Slider.js +162 -0
  56. package/dist/components/StatusBar.d.ts +1 -0
  57. package/dist/components/StatusBar.js +19 -0
  58. package/dist/components/StyledText.d.ts +161 -0
  59. package/dist/components/StyledText.js +193 -0
  60. package/dist/components/Switch.d.ts +44 -0
  61. package/dist/components/Switch.js +129 -0
  62. package/dist/components/Tabs.d.ts +31 -0
  63. package/dist/components/Tabs.js +127 -0
  64. package/dist/components/TextInput.d.ts +120 -0
  65. package/dist/components/TextInput.js +263 -0
  66. package/dist/components/Toggle.d.ts +106 -0
  67. package/dist/components/Toggle.js +150 -0
  68. package/dist/components/ToggleGroup.d.ts +80 -0
  69. package/dist/components/ToggleGroup.js +189 -0
  70. package/dist/components/Tooltip.d.ts +121 -0
  71. package/dist/components/Tooltip.js +132 -0
  72. package/dist/components/index.d.ts +35 -0
  73. package/dist/components/index.js +35 -0
  74. package/dist/constants/colors.d.ts +82 -0
  75. package/dist/constants/colors.js +116 -0
  76. package/dist/constants/fonts.d.ts +32 -0
  77. package/dist/constants/fonts.js +91 -0
  78. package/dist/constants/index.d.ts +3 -0
  79. package/dist/constants/index.js +3 -0
  80. package/dist/constants/spacing.d.ts +40 -0
  81. package/dist/constants/spacing.js +48 -0
  82. package/dist/hooks/index.d.ts +6 -0
  83. package/dist/hooks/index.js +6 -0
  84. package/dist/hooks/useDimensions.d.ts +19 -0
  85. package/dist/hooks/useDimensions.js +55 -0
  86. package/dist/hooks/useReduceMotion.d.ts +5 -0
  87. package/dist/hooks/useReduceMotion.js +64 -0
  88. package/dist/hooks/useResources.d.ts +12 -0
  89. package/dist/hooks/useResources.js +56 -0
  90. package/dist/hooks/useScalePress.d.ts +57 -0
  91. package/dist/hooks/useScalePress.js +55 -0
  92. package/dist/hooks/useStaggeredEntrance.d.ts +67 -0
  93. package/dist/hooks/useStaggeredEntrance.js +74 -0
  94. package/dist/hooks/useTheme.d.ts +88 -0
  95. package/dist/hooks/useTheme.js +328 -0
  96. package/dist/index.d.ts +5 -0
  97. package/dist/index.js +5 -0
  98. package/dist/lib/animations.d.ts +1 -0
  99. package/dist/lib/animations.js +3 -0
  100. package/dist/lib/haptics.d.ts +3 -0
  101. package/dist/lib/haptics.js +29 -0
  102. package/dist/lib/index.d.ts +3 -0
  103. package/dist/lib/index.js +3 -0
  104. package/dist/lib/sentry.d.ts +16 -0
  105. package/dist/lib/sentry.js +55 -0
  106. package/dist/state/globalUIStore.d.ts +30 -0
  107. package/dist/state/globalUIStore.js +8 -0
  108. package/dist/state/index.d.ts +2 -0
  109. package/dist/state/index.js +2 -0
  110. package/dist/state/themeStore.d.ts +6 -0
  111. package/dist/state/themeStore.js +38 -0
  112. 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 {};