@mrmeg/expo-ui 0.6.1 → 0.7.1
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/LLM_USAGE.md +9 -6
- package/README.md +11 -7
- package/dist/components/Accordion.js +21 -16
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +2 -2
- package/dist/components/Badge.d.ts +3 -2
- package/dist/components/Badge.js +4 -3
- package/dist/components/BottomSheet.js +31 -29
- package/dist/components/BottomSheetKeyboard.d.ts +7 -0
- package/dist/components/BottomSheetKeyboard.js +35 -0
- package/dist/components/Button.d.ts +55 -13
- package/dist/components/Button.js +72 -28
- package/dist/components/Card.js +8 -10
- package/dist/components/Checkbox.js +22 -25
- package/dist/components/Collapsible.js +3 -7
- package/dist/components/Dialog.js +1 -1
- package/dist/components/DismissKeyboard.js +3 -3
- package/dist/components/Drawer.js +21 -10
- package/dist/components/DropdownMenu.d.ts +3 -2
- package/dist/components/DropdownMenu.js +29 -29
- package/dist/components/EmptyState.js +1 -1
- package/dist/components/InputOTP.js +16 -40
- package/dist/components/Notification.js +106 -27
- package/dist/components/Popover.js +1 -1
- package/dist/components/Progress.d.ts +2 -2
- package/dist/components/Progress.js +36 -34
- package/dist/components/RadioGroup.js +22 -20
- package/dist/components/Select.js +30 -20
- package/dist/components/Skeleton.js +6 -6
- package/dist/components/Slider.js +90 -97
- package/dist/components/StyledText.context.d.ts +6 -0
- package/dist/components/StyledText.context.js +5 -0
- package/dist/components/StyledText.d.ts +7 -58
- package/dist/components/StyledText.js +8 -28
- package/dist/components/Switch.js +30 -26
- package/dist/components/Tabs.d.ts +23 -3
- package/dist/components/Tabs.js +39 -17
- package/dist/components/TextInput.d.ts +6 -2
- package/dist/components/TextInput.js +6 -7
- package/dist/components/Toggle.js +12 -7
- package/dist/components/ToggleGroup.js +17 -11
- package/dist/components/Tooltip.js +1 -1
- package/dist/hooks/useDimensions.js +25 -26
- package/dist/hooks/useReduceMotion.d.ts +5 -1
- package/dist/hooks/useReduceMotion.js +46 -41
- package/dist/hooks/useResources.js +6 -1
- package/dist/hooks/useScalePress.d.ts +6 -5
- package/dist/hooks/useScalePress.js +25 -21
- package/dist/hooks/useStaggeredEntrance.d.ts +9 -8
- package/dist/hooks/useStaggeredEntrance.js +48 -21
- package/dist/state/globalUIStore.d.ts +23 -16
- package/dist/state/themeColorScope.js +3 -3
- package/llms-full.md +5 -6
- package/llms.txt +2 -2
- package/package.json +8 -6
package/LLM_USAGE.md
CHANGED
|
@@ -68,10 +68,9 @@ export function RootLayout() {
|
|
|
68
68
|
`AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`,
|
|
69
69
|
`SelectContent`, or `Tooltip`.
|
|
70
70
|
|
|
71
|
-
On native, `BottomSheet.Content` avoids the soft keyboard by default
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`avoidKeyboard={false}` to opt out for a specific sheet.
|
|
71
|
+
On native, `BottomSheet.Content` avoids the soft keyboard by default with
|
|
72
|
+
React Native keyboard events. Pass `avoidKeyboard={false}` to opt out for a
|
|
73
|
+
specific sheet.
|
|
75
74
|
|
|
76
75
|
i18n is optional. Do not add app-level i18n setup just to use this package.
|
|
77
76
|
Plain children and `text` props work without `i18next` or `react-i18next`.
|
|
@@ -168,7 +167,7 @@ Use this table before creating a new app-local primitive.
|
|
|
168
167
|
|-----------|---------|----------------------|--------------------------|
|
|
169
168
|
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections |
|
|
170
169
|
| `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs |
|
|
171
|
-
| `AnimatedView` | Entrance and visibility animation | Hand-rolled
|
|
170
|
+
| `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
172
171
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
173
172
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms |
|
|
174
173
|
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs |
|
|
@@ -185,7 +184,7 @@ Use this table before creating a new app-local primitive.
|
|
|
185
184
|
| `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
|
|
186
185
|
| `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
|
|
187
186
|
| `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms |
|
|
188
|
-
| `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, bottom-position alerts |
|
|
187
|
+
| `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, bottom-position alerts |
|
|
189
188
|
| `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls |
|
|
190
189
|
| `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion |
|
|
191
190
|
| `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer |
|
|
@@ -260,5 +259,9 @@ globalUIStore.getState().show({
|
|
|
260
259
|
type: "success",
|
|
261
260
|
title: "Saved",
|
|
262
261
|
messages: ["Your changes were saved."],
|
|
262
|
+
action: {
|
|
263
|
+
label: "View",
|
|
264
|
+
onPress: openSavedItem,
|
|
265
|
+
},
|
|
263
266
|
});
|
|
264
267
|
```
|
package/README.md
CHANGED
|
@@ -29,12 +29,12 @@ bun add @mrmeg/expo-ui
|
|
|
29
29
|
|
|
30
30
|
Consumers must also install the native and Expo peer dependencies listed in
|
|
31
31
|
`package.json`. The tested baseline is Expo SDK 55 with React 19.2, React
|
|
32
|
-
Native 0.83, React Native Web 0.21
|
|
32
|
+
Native 0.83, and React Native Web 0.21. UI animations and keyboard-aware
|
|
33
|
+
sheet offsets use React Native Animated by default.
|
|
33
34
|
`@rn-primitives/*` packages are managed by `@mrmeg/expo-ui` because they are
|
|
34
35
|
implementation details of the exported UI components. Native bottom sheet
|
|
35
|
-
keyboard avoidance uses
|
|
36
|
-
|
|
37
|
-
inputs. i18n setup is optional; plain text and children render without
|
|
36
|
+
keyboard avoidance uses React Native keyboard events. i18n setup is optional;
|
|
37
|
+
plain text and children render without
|
|
38
38
|
`i18next` or `react-i18next`. Start
|
|
39
39
|
consumer apps from the same Expo SDK family or update the package and peer
|
|
40
40
|
ranges deliberately. Keep npm auth tokens in developer or CI configuration,
|
|
@@ -207,7 +207,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
207
207
|
|-----------|---------|----------------------|--------------------------|
|
|
208
208
|
| `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` | Multi-section disclosure | Custom FAQ/settings expanders | FAQ lists, grouped settings, help sections, dense detail pages |
|
|
209
209
|
| `Alert` | Cross-platform imperative alerts | Direct `window.alert` or duplicated RN/web branching | Confirm destructive actions, native alert dialogs, simple blocking messages |
|
|
210
|
-
| `AnimatedView` | Entrance and visibility animation | Hand-rolled
|
|
210
|
+
| `AnimatedView` | Entrance and visibility animation | Hand-rolled one-off Animated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
211
211
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
212
212
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, keyboard-aware quick edit forms, contextual details |
|
|
213
213
|
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs, icon-accessory buttons; loading state preserves resting width |
|
|
@@ -224,7 +224,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
224
224
|
| `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
|
|
225
225
|
| `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
|
|
226
226
|
| `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms, auth panels |
|
|
227
|
-
| `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, loading toast, bottom-position alerts |
|
|
227
|
+
| `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, loading toast, bottom-position alerts |
|
|
228
228
|
| `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls, small forms |
|
|
229
229
|
| `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion, long-running task state |
|
|
230
230
|
| `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer, preference setting |
|
|
@@ -272,7 +272,7 @@ Use `Button.preset`, not `variant`. `default` is the neutral primary action, `se
|
|
|
272
272
|
|
|
273
273
|
Use `StyledText` or its aliases instead of raw `Text` whenever the text is part of app UI. Use `TextInput` for labeled fields because it already owns label, helper text, error text, clear buttons, password visibility, numeric filtering, and left/right elements.
|
|
274
274
|
|
|
275
|
-
Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native,
|
|
275
|
+
Mount `UIProvider` once near the root before using `Dialog`, `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`, `Tooltip`, or package notifications. On native, `BottomSheet.Content` listens to React Native keyboard events when `avoidKeyboard` is enabled; it defaults to `true` and can be disabled per sheet. Trigger transient feedback from `globalUIStore`.
|
|
276
276
|
|
|
277
277
|
Use `Skeleton` components for loading content with stable dimensions, `EmptyState` for no-data/recoverable errors, `Alert` for blocking confirm/alert dialogs, and `Notification` for transient global feedback.
|
|
278
278
|
|
|
@@ -332,6 +332,10 @@ globalUIStore.getState().show({
|
|
|
332
332
|
type: "success",
|
|
333
333
|
title: "Saved",
|
|
334
334
|
messages: ["Your changes were saved."],
|
|
335
|
+
action: {
|
|
336
|
+
label: "View",
|
|
337
|
+
onPress: openSavedItem,
|
|
338
|
+
},
|
|
335
339
|
});
|
|
336
340
|
```
|
|
337
341
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import { Platform, Pressable, View } from "react-native";
|
|
4
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Platform, Pressable, View } from "react-native";
|
|
5
4
|
import { Icon } from "./Icon.js";
|
|
6
|
-
import { TextClassContext, TextSelectabilityContext } from "./StyledText.
|
|
5
|
+
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
9
|
import * as AccordionPrimitive from "@rn-primitives/accordion";
|
|
10
10
|
function normalizeSingleValue(value) {
|
|
@@ -100,20 +100,25 @@ function AccordionTrigger({ children, style: styleOverride, ...props }) {
|
|
|
100
100
|
const { theme } = useTheme();
|
|
101
101
|
const reduceMotion = useReducedMotion();
|
|
102
102
|
const { isExpanded } = AccordionPrimitive.useItemContext();
|
|
103
|
-
const rotation =
|
|
103
|
+
const rotation = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
const target = isExpanded ? 1 : 0;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
Animated.timing(rotation, {
|
|
107
|
+
toValue: target,
|
|
108
|
+
duration: reduceMotion ? 0 : isExpanded ? 200 : 150,
|
|
109
|
+
useNativeDriver: true,
|
|
110
|
+
}).start();
|
|
111
|
+
}, [isExpanded, reduceMotion, rotation]);
|
|
112
|
+
const chevronStyle = {
|
|
113
|
+
transform: [
|
|
114
|
+
{
|
|
115
|
+
rotate: rotation.interpolate({
|
|
116
|
+
inputRange: [0, 1],
|
|
117
|
+
outputRange: ["0deg", "180deg"],
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
117
122
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(AccordionPrimitive.Trigger, { ...props, asChild: true, children: _jsxs(Trigger, { style: [
|
|
118
123
|
{
|
|
119
124
|
flexDirection: "row",
|
|
@@ -25,7 +25,7 @@ interface AnimatedViewProps extends ViewProps {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Cross-Platform Animated View Component
|
|
28
|
-
* Uses
|
|
28
|
+
* Uses React Native Animated for lightweight cross-platform animations
|
|
29
29
|
*
|
|
30
30
|
* Features:
|
|
31
31
|
* - Multiple animation types (fade, fadeSlideUp, fadeSlideDown, scale)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import Animated from "react-native
|
|
2
|
+
import { Animated } from "react-native";
|
|
3
3
|
import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance.js";
|
|
4
4
|
/**
|
|
5
5
|
* Cross-Platform Animated View Component
|
|
6
|
-
* Uses
|
|
6
|
+
* Uses React Native Animated for lightweight cross-platform animations
|
|
7
7
|
*
|
|
8
8
|
* Features:
|
|
9
9
|
* - Multiple animation types (fade, fadeSlideUp, fadeSlideDown, scale)
|
|
@@ -2,7 +2,8 @@ import React from "react";
|
|
|
2
2
|
import { StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
export type BadgeVariant = "default" | "secondary" | "outline" | "destructive";
|
|
4
4
|
export interface BadgeProps {
|
|
5
|
-
children
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
text?: string;
|
|
6
7
|
variant?: BadgeVariant;
|
|
7
8
|
style?: StyleProp<ViewStyle>;
|
|
8
9
|
}
|
|
@@ -19,5 +20,5 @@ export interface BadgeProps {
|
|
|
19
20
|
* <Badge variant="destructive">Error</Badge>
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
|
-
declare function Badge({ children, variant, style: styleOverride }: BadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
declare function Badge({ children, text, variant, style: styleOverride }: BadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
23
24
|
export { Badge };
|
package/dist/components/Badge.js
CHANGED
|
@@ -17,9 +17,10 @@ import { StyledText } from "./StyledText.js";
|
|
|
17
17
|
* <Badge variant="destructive">Error</Badge>
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
function Badge({ children, variant = "default", style: styleOverride }) {
|
|
20
|
+
function Badge({ children, text, variant = "default", style: styleOverride }) {
|
|
21
21
|
const { theme } = useTheme();
|
|
22
22
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
23
|
+
const badgeContent = text ?? children;
|
|
23
24
|
const textStyle = [
|
|
24
25
|
styles.text,
|
|
25
26
|
variant === "default" && { color: theme.colors.primaryForeground },
|
|
@@ -27,9 +28,9 @@ function Badge({ children, variant = "default", style: styleOverride }) {
|
|
|
27
28
|
variant === "outline" && { color: theme.colors.foreground },
|
|
28
29
|
variant === "destructive" && { color: theme.colors.destructiveForeground },
|
|
29
30
|
];
|
|
30
|
-
const normalizedChildren = React.Children.toArray(
|
|
31
|
+
const normalizedChildren = React.Children.toArray(badgeContent);
|
|
31
32
|
const hasOnlyTextChildren = normalizedChildren.every((child) => typeof child === "string" || typeof child === "number");
|
|
32
|
-
const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(
|
|
33
|
+
const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(badgeContent, (child) => {
|
|
33
34
|
if (typeof child === "string" || typeof child === "number") {
|
|
34
35
|
return _jsx(StyledText, { selectable: false, style: textStyle, children: child });
|
|
35
36
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext,
|
|
3
|
-
import { View, Pressable, Animated, StyleSheet, Platform,
|
|
2
|
+
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { View, Pressable, Animated, StyleSheet, Platform, PanResponder, ScrollView, } from "react-native";
|
|
4
4
|
import { Portal } from "@rn-primitives/portal";
|
|
5
5
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
6
6
|
import { Pressable as SlotPressable } from "@rn-primitives/slot";
|
|
7
|
-
import { KeyboardController, useKeyboardAnimation } from "react-native-keyboard-controller";
|
|
8
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
|
+
import { useDimensions } from "../hooks/useDimensions.js";
|
|
9
9
|
import { spacing } from "../constants/spacing.js";
|
|
10
10
|
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
11
|
-
import { TextColorContext, TextClassContext } from "./StyledText.
|
|
11
|
+
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
12
12
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
13
|
+
import { BottomSheetKeyboardController, useBottomSheetKeyboardAnimation, } from "./BottomSheetKeyboard.js";
|
|
13
14
|
/**
|
|
14
15
|
* BottomSheet Component with Sub-components
|
|
15
16
|
*
|
|
@@ -44,7 +45,7 @@ const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fr
|
|
|
44
45
|
// ============================================================================
|
|
45
46
|
const BottomSheetContext = createContext(null);
|
|
46
47
|
function useBottomSheetContext() {
|
|
47
|
-
const context =
|
|
48
|
+
const context = use(BottomSheetContext);
|
|
48
49
|
if (!context) {
|
|
49
50
|
throw new Error("BottomSheet components must be used within a BottomSheet");
|
|
50
51
|
}
|
|
@@ -61,15 +62,14 @@ function BottomSheetPanel({ accessibilityViewIsModal, children, panHandlers, she
|
|
|
61
62
|
], accessibilityViewIsModal: accessibilityViewIsModal, ...panHandlers, ...props, children: children }));
|
|
62
63
|
}
|
|
63
64
|
function KeyboardAvoidingBottomSheetPanel(props) {
|
|
64
|
-
const { height: keyboardHeight } =
|
|
65
|
+
const { height: keyboardHeight } = useBottomSheetKeyboardAnimation();
|
|
65
66
|
const composedTranslateY = useMemo(() => Animated.add(props.translateY, keyboardHeight), [keyboardHeight, props.translateY]);
|
|
66
67
|
return _jsx(BottomSheetPanel, { ...props, translateY: composedTranslateY });
|
|
67
68
|
}
|
|
68
69
|
// ============================================================================
|
|
69
70
|
// Utility Functions
|
|
70
71
|
// ============================================================================
|
|
71
|
-
function resolveSnapPoints(points) {
|
|
72
|
-
const screenHeight = Dimensions.get("window").height;
|
|
72
|
+
function resolveSnapPoints(points, screenHeight) {
|
|
73
73
|
return points.map((p) => {
|
|
74
74
|
if (typeof p === "number")
|
|
75
75
|
return p;
|
|
@@ -90,7 +90,9 @@ function BottomSheetRoot({ open: controlledOpen, onOpenChange: controlledOnOpenC
|
|
|
90
90
|
const [internalOpen, dispatch] = useReducer(sheetReducer, defaultOpen);
|
|
91
91
|
const isControlled = controlledOpen !== undefined;
|
|
92
92
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
93
|
-
|
|
93
|
+
// useDimensions reacts to rotation / split-screen, unlike Dimensions.get.
|
|
94
|
+
const { height: screenHeight } = useDimensions();
|
|
95
|
+
const snapPoints = resolveSnapPoints(rawSnapPoints, screenHeight);
|
|
94
96
|
const toggle = () => {
|
|
95
97
|
if (isControlled) {
|
|
96
98
|
controlledOnOpenChange?.(!controlledOpen);
|
|
@@ -149,8 +151,18 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
149
151
|
const maxHeight = Math.max(...snapPoints);
|
|
150
152
|
// With bottom:0 positioning, translateY=0 means visible, translateY=maxHeight means hidden below
|
|
151
153
|
const closedPosition = maxHeight;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
// Initialize lazily so each Animated.Value is allocated once on first render
|
|
155
|
+
// instead of being rebuilt and discarded on every render.
|
|
156
|
+
const translateYRef = useRef(null);
|
|
157
|
+
if (translateYRef.current === null) {
|
|
158
|
+
translateYRef.current = new Animated.Value(open ? 0 : closedPosition);
|
|
159
|
+
}
|
|
160
|
+
const translateY = translateYRef.current;
|
|
161
|
+
const backdropOpacityRef = useRef(null);
|
|
162
|
+
if (backdropOpacityRef.current === null) {
|
|
163
|
+
backdropOpacityRef.current = new Animated.Value(open ? 1 : 0);
|
|
164
|
+
}
|
|
165
|
+
const backdropOpacity = backdropOpacityRef.current;
|
|
154
166
|
const [isVisible, setIsVisible] = useState(open);
|
|
155
167
|
const lastOpenRef = useRef(null);
|
|
156
168
|
const runningAnimationRef = useRef(null);
|
|
@@ -158,9 +170,6 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
158
170
|
// Track which snap we're at
|
|
159
171
|
const currentSnapRef = useRef(snapPoints.length - 1);
|
|
160
172
|
const textColor = theme.colors.foreground;
|
|
161
|
-
// ------------------------------------------------------------------
|
|
162
|
-
// Shared snap/close logic used by both native PanResponder and web drag
|
|
163
|
-
// ------------------------------------------------------------------
|
|
164
173
|
const handleDragRelease = useCallback((dragDistance, velocity) => {
|
|
165
174
|
const visibleHeight = currentHeightRef.current - dragDistance;
|
|
166
175
|
if (velocity > velocityThreshold / 1000 || dragDistance > currentHeightRef.current * 0.4) {
|
|
@@ -241,12 +250,9 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
241
250
|
}, [translateY, backdropOpacity, maxHeight]);
|
|
242
251
|
const dismissKeyboardForDrag = useCallback(() => {
|
|
243
252
|
if (Platform.OS !== "web" && dismissKeyboardOnDrag) {
|
|
244
|
-
void
|
|
253
|
+
void BottomSheetKeyboardController.dismiss();
|
|
245
254
|
}
|
|
246
255
|
}, [dismissKeyboardOnDrag]);
|
|
247
|
-
// ------------------------------------------------------------------
|
|
248
|
-
// Trigger animation during render if open changed
|
|
249
|
-
// ------------------------------------------------------------------
|
|
250
256
|
if (open !== lastOpenRef.current) {
|
|
251
257
|
const previousOpen = lastOpenRef.current;
|
|
252
258
|
lastOpenRef.current = open;
|
|
@@ -304,9 +310,6 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
304
310
|
});
|
|
305
311
|
}
|
|
306
312
|
}
|
|
307
|
-
// ------------------------------------------------------------------
|
|
308
|
-
// Native: PanResponder for swipe gestures on the whole sheet
|
|
309
|
-
// ------------------------------------------------------------------
|
|
310
313
|
const panResponder = useMemo(() => Platform.OS !== "web" && swipeEnabled
|
|
311
314
|
? PanResponder.create({
|
|
312
315
|
onStartShouldSetPanResponder: () => false,
|
|
@@ -325,9 +328,6 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
325
328
|
},
|
|
326
329
|
})
|
|
327
330
|
: null, [dismissKeyboardForDrag, handleDragMove, handleDragRelease, swipeEnabled]);
|
|
328
|
-
// ------------------------------------------------------------------
|
|
329
|
-
// Web: drag context provides callbacks for Handle's pointer events
|
|
330
|
-
// ------------------------------------------------------------------
|
|
331
331
|
const dragContextValue = Platform.OS === "web" && swipeEnabled
|
|
332
332
|
? {
|
|
333
333
|
onDragMove: handleDragMove,
|
|
@@ -363,25 +363,27 @@ function BottomSheetContent({ swipeEnabled = true, velocityThreshold = 500, avoi
|
|
|
363
363
|
const PanelComponent = Platform.OS !== "web" && avoidKeyboard
|
|
364
364
|
? KeyboardAvoidingBottomSheetPanel
|
|
365
365
|
: BottomSheetPanel;
|
|
366
|
-
|
|
366
|
+
return (_jsx(BottomSheetContentPortal, { sheetContext: sheetContext, theme: theme, backdropOpacity: backdropOpacity, onBackdropPress: handleBackdropPress, PanelComponent: PanelComponent, sheetStyle: sheetStyle, styleOverride: styleOverride, translateY: translateY, panHandlers: panResponder ? panResponder.panHandlers : undefined, panelProps: props, dragContextValue: dragContextValue, sheetContent: sheetContent }));
|
|
367
|
+
}
|
|
368
|
+
function BottomSheetContentPortal({ sheetContext, theme, backdropOpacity, onBackdropPress, PanelComponent, sheetStyle, styleOverride, translateY, panHandlers, panelProps, dragContextValue, sheetContent, }) {
|
|
369
|
+
return (_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: [
|
|
367
370
|
StyleSheet.absoluteFill,
|
|
368
371
|
{
|
|
369
372
|
backgroundColor: theme.colors.overlay,
|
|
370
373
|
opacity: backdropOpacity,
|
|
371
374
|
},
|
|
372
375
|
Platform.OS === "web" && { zIndex: 50 },
|
|
373
|
-
], children: _jsx(Pressable, { style: StyleSheet.absoluteFill, onPress:
|
|
376
|
+
], children: _jsx(Pressable, { style: StyleSheet.absoluteFill, onPress: onBackdropPress }) }), _jsx(PanelComponent, { sheetStyle: sheetStyle, styleOverride: styleOverride, translateY: translateY, accessibilityViewIsModal: true, ...(Platform.OS === "web" && {
|
|
374
377
|
role: "dialog",
|
|
375
378
|
"aria-modal": true,
|
|
376
|
-
}), panHandlers:
|
|
377
|
-
return contentElement;
|
|
379
|
+
}), panHandlers: panHandlers, ...panelProps, children: dragContextValue ? (_jsx(DragContext.Provider, { value: dragContextValue, children: sheetContent })) : (sheetContent) })] }) }) }) }));
|
|
378
380
|
}
|
|
379
381
|
// ============================================================================
|
|
380
382
|
// Handle
|
|
381
383
|
// ============================================================================
|
|
382
384
|
function BottomSheetHandle({ style }) {
|
|
383
385
|
const { theme } = useTheme();
|
|
384
|
-
const dragCtx =
|
|
386
|
+
const dragCtx = use(DragContext);
|
|
385
387
|
// Web pointer-event drag — attaches move/up listeners on document
|
|
386
388
|
const dragStartY = useRef(0);
|
|
387
389
|
const lastTimestamp = useRef(0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, Keyboard, Platform } from "react-native";
|
|
3
|
+
function animateKeyboardOffset(value, toValue, duration = 180) {
|
|
4
|
+
Animated.timing(value, {
|
|
5
|
+
toValue,
|
|
6
|
+
duration,
|
|
7
|
+
useNativeDriver: true,
|
|
8
|
+
}).start();
|
|
9
|
+
}
|
|
10
|
+
export function useBottomSheetKeyboardAnimation() {
|
|
11
|
+
const keyboardHeight = useRef(new Animated.Value(0)).current;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (Platform.OS === "web") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
17
|
+
const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
18
|
+
const showSubscription = Keyboard.addListener(showEvent, (event) => {
|
|
19
|
+
animateKeyboardOffset(keyboardHeight, -event.endCoordinates.height, event.duration || 180);
|
|
20
|
+
});
|
|
21
|
+
const hideSubscription = Keyboard.addListener(hideEvent, (event) => {
|
|
22
|
+
animateKeyboardOffset(keyboardHeight, 0, event.duration || 160);
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
showSubscription.remove();
|
|
26
|
+
hideSubscription.remove();
|
|
27
|
+
};
|
|
28
|
+
}, [keyboardHeight]);
|
|
29
|
+
return { height: keyboardHeight };
|
|
30
|
+
}
|
|
31
|
+
export const BottomSheetKeyboardController = {
|
|
32
|
+
dismiss() {
|
|
33
|
+
Keyboard.dismiss();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { ComponentType } from "react";
|
|
2
2
|
import { PressableProps, PressableStateCallbackType, DimensionValue, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
3
3
|
import { TextProps } from "./StyledText";
|
|
4
|
+
import { type IconProps } from "./Icon";
|
|
4
5
|
/**
|
|
5
6
|
* Button variants
|
|
6
7
|
*/
|
|
@@ -115,26 +116,67 @@ export interface ButtonProps extends PressableProps {
|
|
|
115
116
|
*
|
|
116
117
|
* Usage:
|
|
117
118
|
* ```tsx
|
|
118
|
-
* //
|
|
119
|
-
* <Button onPress={handler}
|
|
120
|
-
* <SansSerifBoldText>Click Me</SansSerifBoldText>
|
|
121
|
-
* </Button>
|
|
119
|
+
* // Simplest path — plain label via the `text` prop
|
|
120
|
+
* <Button onPress={handler} text="Click Me" />
|
|
122
121
|
*
|
|
123
122
|
* // Different variants
|
|
124
|
-
* <Button preset="outline" onPress={handler}
|
|
125
|
-
* <Button preset="ghost" onPress={handler}
|
|
126
|
-
* <Button preset="destructive" onPress={handler}
|
|
123
|
+
* <Button preset="outline" onPress={handler} text="Outline" />
|
|
124
|
+
* <Button preset="ghost" onPress={handler} text="Ghost" />
|
|
125
|
+
* <Button preset="destructive" onPress={handler} text="Delete" />
|
|
127
126
|
*
|
|
128
127
|
* // Different sizes
|
|
129
|
-
* <Button size="sm" onPress={handler}
|
|
130
|
-
* <Button size="lg" onPress={handler}
|
|
128
|
+
* <Button size="sm" onPress={handler} text="Small" />
|
|
129
|
+
* <Button size="lg" onPress={handler} text="Large" />
|
|
131
130
|
*
|
|
132
131
|
* // Loading state
|
|
133
|
-
* <Button loading onPress={handler}
|
|
132
|
+
* <Button loading onPress={handler} text="Processing..." />
|
|
134
133
|
*
|
|
135
134
|
* // Full width
|
|
136
|
-
* <Button fullWidth onPress={handler}
|
|
135
|
+
* <Button fullWidth onPress={handler} text="Submit" />
|
|
136
|
+
*
|
|
137
|
+
* // Composed content — icon + label via subcomponents
|
|
138
|
+
* <Button onPress={handler}>
|
|
139
|
+
* <Button.Icon name="heart" />
|
|
140
|
+
* <Button.Text>Like</Button.Text>
|
|
141
|
+
* </Button>
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function ButtonRoot(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
145
|
+
/**
|
|
146
|
+
* Button.Text
|
|
147
|
+
* Text content for a Button. Inherits the button's control typography, color,
|
|
148
|
+
* and non-selectable behavior from context, so callers state their intent
|
|
149
|
+
* explicitly instead of relying on the component to inspect `typeof children`.
|
|
150
|
+
*
|
|
151
|
+
* ```tsx
|
|
152
|
+
* <Button onPress={save}>
|
|
153
|
+
* <Button.Text>Save</Button.Text>
|
|
154
|
+
* </Button>
|
|
137
155
|
* ```
|
|
138
156
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
declare function ButtonText(props: TextProps): import("react/jsx-runtime").JSX.Element;
|
|
158
|
+
/**
|
|
159
|
+
* Button.Icon
|
|
160
|
+
* Icon content for a Button. Defaults its color to the button's text color
|
|
161
|
+
* (from context) and is decorative by default, since the label conveys meaning.
|
|
162
|
+
*
|
|
163
|
+
* ```tsx
|
|
164
|
+
* <Button onPress={like}>
|
|
165
|
+
* <Button.Icon name="heart" />
|
|
166
|
+
* <Button.Text>Like</Button.Text>
|
|
167
|
+
* </Button>
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare function ButtonIcon(props: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
171
|
+
/**
|
|
172
|
+
* Button with explicit subcomponents.
|
|
173
|
+
* - `Button.Text` for label text (inherits control typography)
|
|
174
|
+
* - `Button.Icon` for icons (inherits the button's text color)
|
|
175
|
+
*
|
|
176
|
+
* The `tx`/`text` props remain the simplest path for plain labels.
|
|
177
|
+
*/
|
|
178
|
+
declare const Button: typeof ButtonRoot & {
|
|
179
|
+
Text: typeof ButtonText;
|
|
180
|
+
Icon: typeof ButtonIcon;
|
|
181
|
+
};
|
|
182
|
+
export { Button, ButtonText, ButtonIcon };
|