@mrmeg/expo-ui 0.27.0 → 0.28.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/CHANGELOG.md +712 -0
- package/LLM_USAGE.md +260 -32
- package/README.md +558 -101
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +66 -9
- package/dist/components/BottomSheet.js +143 -39
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +64 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +178 -29
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +7 -3
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +74 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +23 -32
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +73 -27
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/bottomSheetHostSupport.d.ts +46 -0
- package/dist/components/bottomSheetHostSupport.js +67 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.d.ts +25 -0
- package/dist/components/keyboardDismiss.js +60 -3
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -4
- package/dist/lib/index.js +2 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/stateSurface.d.ts +28 -0
- package/dist/lib/stateSurface.js +30 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
|
@@ -1,16 +1,152 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { Platform, StyleSheet, View } from "react-native";
|
|
4
|
-
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
3
|
+
import { Modal, Platform, StyleSheet, View } from "react-native";
|
|
5
4
|
import * as DialogPrimitive from "@rn-primitives/dialog";
|
|
6
5
|
import * as AlertDialogPrimitive from "@rn-primitives/alert-dialog";
|
|
7
6
|
import { AnimatedView } from "./AnimatedView.js";
|
|
8
|
-
import {
|
|
7
|
+
import { KeyboardAvoidingView } from "./KeyboardAvoidingView.js";
|
|
8
|
+
import { useWindowInsets } from "../hooks/useWindowInsets.js";
|
|
9
|
+
import { useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
10
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
9
11
|
import { StyledText } from "./StyledText.js";
|
|
10
12
|
import { useTheme } from "../hooks/useTheme.js";
|
|
13
|
+
import { useShape } from "../hooks/useShape.js";
|
|
11
14
|
import { spacing } from "../constants/spacing.js";
|
|
12
15
|
import { palette } from "../constants/colors.js";
|
|
13
|
-
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Presentation
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/** The Modal follows the app's own orientation mask instead of RN's portrait-only default. */
|
|
20
|
+
const SUPPORTED_ORIENTATIONS = [
|
|
21
|
+
"portrait",
|
|
22
|
+
"portrait-upside-down",
|
|
23
|
+
"landscape",
|
|
24
|
+
"landscape-left",
|
|
25
|
+
"landscape-right",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Presentation for dialog content.
|
|
29
|
+
*
|
|
30
|
+
* iOS presents through React Native's `Modal`, rendered inline where the
|
|
31
|
+
* dialog sits in the tree rather than through the portal host. Until 0.27.1
|
|
32
|
+
* both dialogs rendered into `UIProvider`'s portal host inside
|
|
33
|
+
* react-native-screens' `FullWindowOverlay`, which stacks above native stack
|
|
34
|
+
* modals by adding its container straight to the `UIWindow`. No
|
|
35
|
+
* `UIViewController` sits above that container, and `expo-modules-core`'s
|
|
36
|
+
* `ExpoSwiftUI.HostingView` — which attaches its `UIHostingController` only
|
|
37
|
+
* when `reactViewController()` finds a parent controller — removed the SwiftUI
|
|
38
|
+
* view instead. Every `@expo/ui`-hosted control inside (`TextInput`, `Slider`,
|
|
39
|
+
* `SegmentedControl`) was a zero-height box: no editable element, no focus, no
|
|
40
|
+
* keyboard (fieldnest on iOS 27, `@mrmeg/expo-ui` 0.27.0; reproduced in the
|
|
41
|
+
* template's `Dialog` `form` variant — the accessibility tree listed only the
|
|
42
|
+
* field labels, and the same content mounted with the overlay removed).
|
|
43
|
+
*
|
|
44
|
+
* `Modal` presents a real view controller, so hosted controls mount and take
|
|
45
|
+
* focus on the first tap. RN presents it from the view controller nearest the
|
|
46
|
+
* `Modal`'s own host view, which is why it renders inline: a `Modal` in the
|
|
47
|
+
* root portal host presents from the root controller and silently fails
|
|
48
|
+
* whenever that controller already presents a native stack modal or a sheet
|
|
49
|
+
* (device-verified: the trigger reported expanded, nothing appeared). Inline,
|
|
50
|
+
* the presenter is whatever screen, native stack modal or sheet contains the
|
|
51
|
+
* dialog, and the dialog stacks above it. The trade-off: content hosted by
|
|
52
|
+
* `FullWindowOverlay` (`Drawer`, `Popover`, `Select`, `DropdownMenu`,
|
|
53
|
+
* `Tooltip`) has no view controller either, so a dialog placed inside that
|
|
54
|
+
* content cannot present on iOS — render it at screen level and open it from
|
|
55
|
+
* the item's `onPress`.
|
|
56
|
+
*
|
|
57
|
+
* Android and web render into the portal host, as before.
|
|
58
|
+
*/
|
|
59
|
+
function DialogPresentation({ Portal, open, portalHost, onRequestClose, children, }) {
|
|
60
|
+
if (Platform.OS !== "ios") {
|
|
61
|
+
return _jsx(Portal, { hostName: portalHost, children: children });
|
|
62
|
+
}
|
|
63
|
+
if (!open) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return (_jsx(Modal, { visible: true, transparent: true, animationType: "none", presentationStyle: "overFullScreen", supportedOrientations: SUPPORTED_ORIENTATIONS, onRequestClose: onRequestClose, children: children }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Keyboard avoidance owner for dialog content.
|
|
70
|
+
*
|
|
71
|
+
* Dialog content sits outside `UIProvider`'s root `KeyboardAvoidingView` on
|
|
72
|
+
* both native platforms — the iOS `Modal` above is presented outside it, and
|
|
73
|
+
* on Android the primitive `Portal` renders into `UIProvider`'s `PortalHost`,
|
|
74
|
+
* a sibling of the root avoidance — so nothing else can keep a focused dialog
|
|
75
|
+
* field and the footer above the keyboard. This wrapper pads the centered
|
|
76
|
+
* container by the keyboard height, and the card (capped at 85% of the
|
|
77
|
+
* remaining height) recenters in the space that is left. It is the package
|
|
78
|
+
* `KeyboardAvoidingView` (keyboard-controller on native, which observes the
|
|
79
|
+
* main window's IME on Android, where the portal-hosted dialog lives), so
|
|
80
|
+
* `useKeyboardAvoidance()` is `true` inside `DialogContent` and a
|
|
81
|
+
* `DismissKeyboard` in dialog content adds no second layer. Do not wrap dialog
|
|
82
|
+
* content in another `KeyboardAvoidingView`.
|
|
83
|
+
*
|
|
84
|
+
* Web has no software keyboard and keeps the portal-host tree unchanged.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* On native the overlay (an iOS `Modal`, the Android portal) covers the whole
|
|
88
|
+
* window, so the centered container pads the safe-area insets: `sizer`'s 85 %
|
|
89
|
+
* is then 85 % of the area between the Dynamic Island and the home indicator,
|
|
90
|
+
* not of the screen. Web's overlay is the viewport; nothing to pad.
|
|
91
|
+
*/
|
|
92
|
+
function useDialogSafeAreaPadding() {
|
|
93
|
+
const insets = useWindowInsets();
|
|
94
|
+
if (Platform.OS === "web")
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
paddingTop: insets.top,
|
|
98
|
+
paddingBottom: insets.bottom,
|
|
99
|
+
paddingLeft: insets.left,
|
|
100
|
+
paddingRight: insets.right,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function DialogKeyboardAvoidance({ children }) {
|
|
104
|
+
if (Platform.OS === "web") {
|
|
105
|
+
return _jsx(_Fragment, { children: children });
|
|
106
|
+
}
|
|
107
|
+
return (_jsx(KeyboardAvoidingView, { style: overlayStyles.fill, behavior: "padding", keyboardVerticalOffset: 0, children: children }));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Tap-away keyboard dismissal for dialog content.
|
|
111
|
+
*
|
|
112
|
+
* The iOS `Modal` sits outside any app-level `DismissKeyboard`, and the
|
|
113
|
+
* portal-hosted tree on Android does too, so a tap on the card's dead space
|
|
114
|
+
* (padding, labels, the gap between fields and footer) left the keyboard up.
|
|
115
|
+
* The dialogs carry the same boundary as `DismissKeyboard` and
|
|
116
|
+
* `BottomSheet.Content` (`useKeyboardDismissResponder`): `Close` / `Action` /
|
|
117
|
+
* `Cancel`, buttons and fields win the negotiation and fire on the first tap,
|
|
118
|
+
* and the boundary dismisses on release of an otherwise unclaimed single-finger
|
|
119
|
+
* tap within the travel slop, through the registered field's blur handle with a
|
|
120
|
+
* `KeyboardController.dismiss()` fallback. Inert on web (returns `{}`).
|
|
121
|
+
*
|
|
122
|
+
* Where it sits differs per primitive. `@rn-primitives/dialog`'s native
|
|
123
|
+
* `Content` claims every touch that reaches it (`onStartShouldSetResponder`
|
|
124
|
+
* returning `true`), so the overlay `Pressable`'s `closeOnPress` never fires for
|
|
125
|
+
* a tap inside the card — and that claim ends the bubble negotiation before any
|
|
126
|
+
* ancestor is asked, so a boundary on the centered container was never armed
|
|
127
|
+
* for a card tap (Pixel 6a: dead-space taps left the keyboard up). `Dialog`
|
|
128
|
+
* therefore puts the boundary on the `Content` itself and keeps the claim
|
|
129
|
+
* (`claimsTouch`): the boundary arms, then answers `true` as the primitive
|
|
130
|
+
* did. `@rn-primitives/alert-dialog`'s `Content` claims nothing, so
|
|
131
|
+
* `AlertDialog` keeps the never-claiming boundary on its centered container,
|
|
132
|
+
* which also covers its backdrop (the alert overlay does not close on press).
|
|
133
|
+
* A `Dialog` backdrop tap is still the primitive `Overlay`'s: it closes the
|
|
134
|
+
* dialog and the keyboard drops with it.
|
|
135
|
+
*/
|
|
136
|
+
function useDialogKeyboardDismissBoundary(claimsTouch = false) {
|
|
137
|
+
const boundary = useKeyboardDismissResponder();
|
|
138
|
+
return React.useMemo(() => {
|
|
139
|
+
if (!claimsTouch || Platform.OS === "web")
|
|
140
|
+
return boundary;
|
|
141
|
+
return {
|
|
142
|
+
...boundary,
|
|
143
|
+
onStartShouldSetResponder: (event) => {
|
|
144
|
+
boundary.onStartShouldSetResponder?.(event);
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}, [boundary, claimsTouch]);
|
|
149
|
+
}
|
|
14
150
|
// ============================================================================
|
|
15
151
|
// Dialog
|
|
16
152
|
// ============================================================================
|
|
@@ -26,8 +162,21 @@ function DialogRoot({ children, ...props }) {
|
|
|
26
162
|
}
|
|
27
163
|
function DialogContent({ portalHost, style, children, ...props }) {
|
|
28
164
|
const { theme, getShadowStyle, getContrastingColor } = useTheme();
|
|
165
|
+
const dialogRadius = useShape("dialog")?.borderRadius ?? spacing.radiusLg;
|
|
166
|
+
const safeAreaPadding = useDialogSafeAreaPadding();
|
|
167
|
+
const { open, onOpenChange } = DialogPrimitive.useRootContext();
|
|
168
|
+
const dismissBoundaryProps = useDialogKeyboardDismissBoundary(true);
|
|
29
169
|
const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
|
|
30
|
-
return (_jsx(DialogPrimitive.Portal,
|
|
170
|
+
return (_jsx(DialogPresentation, { Portal: DialogPrimitive.Portal, open: open, portalHost: portalHost, onRequestClose: () => onOpenChange(false), children: _jsx(DialogPrimitive.Overlay
|
|
171
|
+
// On web the primitive wraps the overlay in a react-native-web
|
|
172
|
+
// Pressable whose click handler stops propagation unconditionally.
|
|
173
|
+
// With that Pressable between the overlay and the Radix content,
|
|
174
|
+
// outside presses (mouse and touch) and Escape never dismissed the
|
|
175
|
+
// dialog, so a dialog without a Close button was stuck. `asChild`
|
|
176
|
+
// makes the primitive clone our fade wrapper instead, removing the
|
|
177
|
+
// Pressable; Radix dismissal then works. Native keeps the Pressable,
|
|
178
|
+
// which is what implements closeOnPress there.
|
|
179
|
+
, {
|
|
31
180
|
// On web the primitive wraps the overlay in a react-native-web
|
|
32
181
|
// Pressable whose click handler stops propagation unconditionally.
|
|
33
182
|
// With that Pressable between the overlay and the Radix content,
|
|
@@ -36,32 +185,23 @@ function DialogContent({ portalHost, style, children, ...props }) {
|
|
|
36
185
|
// makes the primitive clone our fade wrapper instead, removing the
|
|
37
186
|
// Pressable; Radix dismissal then works. Native keeps the Pressable,
|
|
38
187
|
// which is what implements closeOnPress there.
|
|
39
|
-
,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// dialog, so a dialog without a Close button was stuck. `asChild`
|
|
45
|
-
// makes the primitive clone our fade wrapper instead, removing the
|
|
46
|
-
// Pressable; Radix dismissal then works. Native keeps the Pressable,
|
|
47
|
-
// which is what implements closeOnPress there.
|
|
48
|
-
asChild: Platform.OS === "web", style: StyleSheet.flatten([
|
|
49
|
-
StyleSheet.absoluteFill,
|
|
50
|
-
{ backgroundColor: theme.colors.overlay },
|
|
51
|
-
Platform.OS === "web" && { zIndex: 50 },
|
|
52
|
-
]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, children: _jsx(View, { style: overlayStyles.centeredContainer, children: _jsx(AnimatedView, { type: "scale", enterDuration: 250, style: overlayStyles.sizer, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(DialogPrimitive.Content, { style: StyleSheet.flatten([
|
|
188
|
+
asChild: Platform.OS === "web", style: StyleSheet.flatten([
|
|
189
|
+
StyleSheet.absoluteFill,
|
|
190
|
+
{ backgroundColor: theme.colors.overlay },
|
|
191
|
+
Platform.OS === "web" && { zIndex: 50 },
|
|
192
|
+
]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, children: _jsx(DialogKeyboardAvoidance, { children: _jsx(View, { style: [overlayStyles.centeredContainer, safeAreaPadding], testID: "dialog-safe-area", children: _jsx(AnimatedView, { type: "scale", enterDuration: 250, style: overlayStyles.sizer, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(DialogPrimitive.Content, { style: StyleSheet.flatten([
|
|
53
193
|
{
|
|
54
194
|
backgroundColor: theme.colors.popover,
|
|
55
195
|
borderColor: theme.colors.border,
|
|
56
196
|
borderWidth: 1,
|
|
57
|
-
borderRadius:
|
|
197
|
+
borderRadius: dialogRadius,
|
|
58
198
|
padding: spacing.dialogPadding,
|
|
59
199
|
gap: spacing.md,
|
|
60
200
|
width: "100%",
|
|
61
201
|
...getShadowStyle("soft"),
|
|
62
202
|
},
|
|
63
203
|
style,
|
|
64
|
-
]), ...props, children: children }) }) }) }) }) }) }) }) }));
|
|
204
|
+
]), ...dismissBoundaryProps, ...props, children: children }) }) }) }) }) }) }) }) }));
|
|
65
205
|
}
|
|
66
206
|
function DialogHeader({ children, style, ...props }) {
|
|
67
207
|
return (_jsx(View, { style: StyleSheet.flatten([{ gap: spacing.xs }, style]), ...props, children: children }));
|
|
@@ -119,17 +259,21 @@ function AlertDialogRoot({ children, ...props }) {
|
|
|
119
259
|
}
|
|
120
260
|
function AlertDialogContent({ portalHost, style, children, ...props }) {
|
|
121
261
|
const { theme, getShadowStyle, getContrastingColor } = useTheme();
|
|
262
|
+
const dialogRadius = useShape("dialog")?.borderRadius ?? spacing.radiusLg;
|
|
263
|
+
const safeAreaPadding = useDialogSafeAreaPadding();
|
|
264
|
+
const { open, onOpenChange } = AlertDialogPrimitive.useRootContext();
|
|
265
|
+
const dismissBoundaryProps = useDialogKeyboardDismissBoundary();
|
|
122
266
|
const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
|
|
123
|
-
return (_jsx(AlertDialogPrimitive.Portal,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
267
|
+
return (_jsx(DialogPresentation, { Portal: AlertDialogPrimitive.Portal, open: open, portalHost: portalHost, onRequestClose: () => onOpenChange(false), children: _jsx(AlertDialogPrimitive.Overlay, { style: StyleSheet.flatten([
|
|
268
|
+
StyleSheet.absoluteFill,
|
|
269
|
+
{ backgroundColor: theme.colors.overlay },
|
|
270
|
+
Platform.OS === "web" && { zIndex: 52 },
|
|
271
|
+
]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, children: _jsx(DialogKeyboardAvoidance, { children: _jsx(View, { style: [overlayStyles.centeredContainer, safeAreaPadding], ...dismissBoundaryProps, children: _jsx(AnimatedView, { type: "scale", enterDuration: 250, style: overlayStyles.sizer, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(AlertDialogPrimitive.Content, { style: StyleSheet.flatten([
|
|
128
272
|
{
|
|
129
273
|
backgroundColor: theme.colors.popover,
|
|
130
274
|
borderColor: theme.colors.border,
|
|
131
275
|
borderWidth: 1,
|
|
132
|
-
borderRadius:
|
|
276
|
+
borderRadius: dialogRadius,
|
|
133
277
|
padding: spacing.dialogPadding,
|
|
134
278
|
gap: spacing.md,
|
|
135
279
|
width: "100%",
|
|
@@ -174,7 +318,12 @@ const AlertDialog = Object.assign(AlertDialogRoot, {
|
|
|
174
318
|
// ============================================================================
|
|
175
319
|
// Shared styles
|
|
176
320
|
// ============================================================================
|
|
177
|
-
const overlayStyles = StyleSheet.create({
|
|
321
|
+
const overlayStyles = /*#__PURE__*/ StyleSheet.create({
|
|
322
|
+
// The keyboard-avoiding wrapper fills the overlay so its bottom padding
|
|
323
|
+
// shrinks the centered container instead of the card.
|
|
324
|
+
fill: {
|
|
325
|
+
flex: 1,
|
|
326
|
+
},
|
|
178
327
|
centeredContainer: {
|
|
179
328
|
flex: 1,
|
|
180
329
|
justifyContent: "center",
|
|
@@ -80,7 +80,7 @@ interface DrawerBodyProps extends ViewProps {
|
|
|
80
80
|
interface DrawerFooterProps extends ViewProps {
|
|
81
81
|
children: React.ReactNode;
|
|
82
82
|
}
|
|
83
|
-
declare function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, side, width, closeOnBackdropPress, variant, collapsedWidth, expandedWidth, expandOnHover, defaultExpanded, expanded: controlledExpanded, onExpandedChange: controlledOnExpandedChange, children, }: DrawerProps): React.JSX.Element;
|
|
83
|
+
declare function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, side, width, closeOnBackdropPress, variant, collapsedWidth, expandedWidth, expandOnHover: expandOnHoverProp, defaultExpanded, expanded: controlledExpanded, onExpandedChange: controlledOnExpandedChange, children, }: DrawerProps): React.JSX.Element;
|
|
84
84
|
declare function DrawerTrigger({ asChild, children, style: styleOverride }: DrawerTriggerProps): React.JSX.Element;
|
|
85
85
|
interface DrawerToggleCollapseProps {
|
|
86
86
|
/** Use child component as the toggle */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, use, useState, useReducer, useRef } from "react";
|
|
2
|
+
import React, { createContext, use, useEffectEvent, useLayoutEffect, useState, useReducer, useRef } from "react";
|
|
3
3
|
import { View, Pressable, Animated, StyleSheet, Platform, PanResponder, } from "react-native";
|
|
4
4
|
import { Portal } from "@rn-primitives/portal";
|
|
5
5
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
@@ -7,9 +7,10 @@ import { Pressable as SlotPressable } from "@rn-primitives/slot";
|
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { useDimensions } from "../hooks/useDimensions.js";
|
|
9
9
|
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
10
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
10
11
|
import { spacing } from "../constants/spacing.js";
|
|
11
12
|
import { durations } from "../constants/motion.js";
|
|
12
|
-
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
13
|
+
import { TextColorContext, TextClassContext } from "./StyledText.context.js";
|
|
13
14
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
14
15
|
import { SansSerifBoldText } from "./StyledText.js";
|
|
15
16
|
/**
|
|
@@ -80,7 +81,10 @@ function drawerReducer(state, action) {
|
|
|
80
81
|
// ============================================================================
|
|
81
82
|
// Drawer Root Component
|
|
82
83
|
// ============================================================================
|
|
83
|
-
function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, side = "left", width = 300, closeOnBackdropPress = true, variant = "overlay", collapsedWidth = 72, expandedWidth = 240, expandOnHover
|
|
84
|
+
function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, side = "left", width = 300, closeOnBackdropPress = true, variant = "overlay", collapsedWidth = 72, expandedWidth = 240, expandOnHover: expandOnHoverProp, defaultExpanded = false, expanded: controlledExpanded, onExpandedChange: controlledOnExpandedChange, children, }) {
|
|
85
|
+
// Resolved in the body rather than as a default parameter: the React
|
|
86
|
+
// Compiler can't reorder a computed default, and skipped the component.
|
|
87
|
+
const expandOnHover = expandOnHoverProp === undefined ? Platform.OS === "web" : expandOnHoverProp;
|
|
84
88
|
// Use reducer for stable state management - dispatch is stable and reducer always gets current state
|
|
85
89
|
const [internalOpen, dispatch] = useReducer(drawerReducer, defaultOpen);
|
|
86
90
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -219,42 +223,43 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
219
223
|
const { open, onOpenChange, side, width, closeOnBackdropPress } = drawerContext;
|
|
220
224
|
const { theme, getShadowStyle } = useTheme();
|
|
221
225
|
const insets = useSafeAreaInsets();
|
|
222
|
-
// Animation values
|
|
223
|
-
//
|
|
226
|
+
// Animation values, allocated once. Both start closed: a drawer that mounts
|
|
227
|
+
// open slides in from there (see the open-change effect below).
|
|
224
228
|
const closedPosition = side === "left" ? -width : width;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
translateXRef.current = new Animated.Value(open ? 0 : closedPosition);
|
|
228
|
-
}
|
|
229
|
-
const translateX = translateXRef.current;
|
|
230
|
-
const backdropOpacityRef = useRef(null);
|
|
231
|
-
if (backdropOpacityRef.current === null) {
|
|
232
|
-
backdropOpacityRef.current = new Animated.Value(open ? 1 : 0);
|
|
233
|
-
}
|
|
234
|
-
const backdropOpacity = backdropOpacityRef.current;
|
|
229
|
+
const translateX = useAnimatedValue(closedPosition);
|
|
230
|
+
const backdropOpacity = useAnimatedValue(0);
|
|
235
231
|
// Track if drawer is actually visible (for unmounting after close animation)
|
|
236
232
|
const [isVisible, setIsVisible] = useState(open);
|
|
233
|
+
// Opening marks the panel visible in the same render, so it stays mounted
|
|
234
|
+
// through the close animation that follows. State adjusted during render
|
|
235
|
+
// (React re-runs the render before committing it), keyed on `open` changing.
|
|
236
|
+
const [previousOpenProp, setPreviousOpenProp] = useState(open);
|
|
237
|
+
if (open !== previousOpenProp) {
|
|
238
|
+
setPreviousOpenProp(open);
|
|
239
|
+
if (open && !isVisible) {
|
|
240
|
+
setIsVisible(true);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
237
243
|
// Track what we last animated to - persists across renders
|
|
238
244
|
const lastOpenRef = useRef(null);
|
|
239
245
|
// Track running animation to properly cancel it
|
|
240
246
|
const runningAnimationRef = useRef(null);
|
|
241
247
|
// Use semantic foreground color for text on background
|
|
242
248
|
const textColor = theme.colors.foreground;
|
|
243
|
-
//
|
|
244
|
-
|
|
249
|
+
// Animate when `open` changes. Runs in the commit's layout phase, before
|
|
250
|
+
// the frame paints, and reads that commit's position and width.
|
|
251
|
+
const animateOpenChange = useEffectEvent((nextOpen) => {
|
|
245
252
|
const previousOpen = lastOpenRef.current;
|
|
246
|
-
|
|
253
|
+
if (nextOpen === previousOpen)
|
|
254
|
+
return;
|
|
255
|
+
lastOpenRef.current = nextOpen;
|
|
247
256
|
// Stop any running animations immediately
|
|
248
257
|
if (runningAnimationRef.current) {
|
|
249
258
|
runningAnimationRef.current.stop();
|
|
250
259
|
runningAnimationRef.current = null;
|
|
251
260
|
}
|
|
252
|
-
if (
|
|
253
|
-
//
|
|
254
|
-
if (!isVisible) {
|
|
255
|
-
setIsVisible(true);
|
|
256
|
-
}
|
|
257
|
-
// If this is first render (previousOpen is null), set initial position
|
|
261
|
+
if (nextOpen) {
|
|
262
|
+
// If this is the first run (previousOpen is null), start from closed.
|
|
258
263
|
// Otherwise animate from current position (handles mid-animation toggle)
|
|
259
264
|
if (previousOpen === null) {
|
|
260
265
|
translateX.setValue(closedPosition);
|
|
@@ -303,9 +308,13 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
303
308
|
}
|
|
304
309
|
});
|
|
305
310
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
311
|
+
});
|
|
312
|
+
useLayoutEffect(() => {
|
|
313
|
+
animateOpenChange(open);
|
|
314
|
+
}, [open]);
|
|
315
|
+
// Create pan responder for swipe gestures (native only). Built once, from
|
|
316
|
+
// the first render's props, like the ref it replaces.
|
|
317
|
+
const [panResponder] = useState(() => Platform.OS !== "web" && swipeEnabled
|
|
309
318
|
? PanResponder.create({
|
|
310
319
|
onStartShouldSetPanResponder: () => false,
|
|
311
320
|
onMoveShouldSetPanResponder: (_evt, gestureState) => {
|
|
@@ -377,7 +386,7 @@ function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, veloc
|
|
|
377
386
|
}
|
|
378
387
|
},
|
|
379
388
|
})
|
|
380
|
-
: null)
|
|
389
|
+
: null);
|
|
381
390
|
// Handle backdrop press
|
|
382
391
|
const handleBackdropPress = () => {
|
|
383
392
|
if (closeOnBackdropPress) {
|
|
@@ -458,44 +467,45 @@ swipeEnabled: _swipeEnabled, swipeThreshold: _swipeThreshold, velocityThreshold:
|
|
|
458
467
|
// `expanded` true→false) while the pointer is still over it, the active hover
|
|
459
468
|
// would instantly re-expand it and the collapse would look like a no-op.
|
|
460
469
|
// Suppress the current hover session in that case; a fresh mouse-enter clears
|
|
461
|
-
// the suppression so peek-on-hover works again.
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
hoverSuppressedRef.current = true;
|
|
470
|
+
// the suppression so peek-on-hover works again. State adjusted during the
|
|
471
|
+
// render that the `expanded` change already triggers: React re-runs that
|
|
472
|
+
// render with the suppression applied before committing it.
|
|
473
|
+
const [hoverSuppressed, setHoverSuppressed] = useState(false);
|
|
474
|
+
const [previousExpanded, setPreviousExpanded] = useState(expanded);
|
|
475
|
+
if (previousExpanded !== expanded) {
|
|
476
|
+
setPreviousExpanded(expanded);
|
|
477
|
+
if (previousExpanded && !expanded && hovered) {
|
|
478
|
+
setHoverSuppressed(true);
|
|
471
479
|
}
|
|
472
480
|
}
|
|
473
|
-
const effectiveExpanded = expanded || (expandOnHover && hovered && !
|
|
481
|
+
const effectiveExpanded = expanded || (expandOnHover && hovered && !hoverSuppressed);
|
|
474
482
|
const textColor = theme.colors.foreground;
|
|
475
483
|
const targetWidth = effectiveExpanded ? expandedWidth : collapsedWidth;
|
|
476
484
|
// Native animates width via Animated.Value (layout prop → useNativeDriver: false).
|
|
477
485
|
// Web sets the width directly and lets the inline CSS `transition` animate it.
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
//
|
|
484
|
-
// mirroring the overlay's lastOpenRef pattern above. Skip the first render:
|
|
485
|
-
// the Animated.Value is already initialized to the current target, so there is
|
|
486
|
-
// nothing to animate toward on mount.
|
|
486
|
+
const widthAnim = useAnimatedValue(targetWidth);
|
|
487
|
+
// Start the native width animation when expansion changes, in the commit's
|
|
488
|
+
// layout phase (before the frame paints), mirroring the overlay's open-change
|
|
489
|
+
// effect above. Skip the first run: the Animated.Value is already
|
|
490
|
+
// initialized to the current target, so there is nothing to animate toward
|
|
491
|
+
// on mount.
|
|
487
492
|
const lastExpandedRef = useRef(null);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
const animateExpandedChange = useEffectEvent((nextExpanded) => {
|
|
494
|
+
if (Platform.OS === "web" || nextExpanded === lastExpandedRef.current)
|
|
495
|
+
return;
|
|
496
|
+
const previousAnimatedExpanded = lastExpandedRef.current;
|
|
497
|
+
lastExpandedRef.current = nextExpanded;
|
|
498
|
+
if (previousAnimatedExpanded !== null) {
|
|
492
499
|
Animated.timing(widthAnim, {
|
|
493
500
|
toValue: targetWidth,
|
|
494
501
|
duration: durations.normal,
|
|
495
502
|
useNativeDriver: false,
|
|
496
503
|
}).start();
|
|
497
504
|
}
|
|
498
|
-
}
|
|
505
|
+
});
|
|
506
|
+
useLayoutEffect(() => {
|
|
507
|
+
animateExpandedChange(effectiveExpanded);
|
|
508
|
+
}, [effectiveExpanded]);
|
|
499
509
|
const shadowStyle = effectiveExpanded
|
|
500
510
|
? StyleSheet.flatten(getShadowStyle("elevated"))
|
|
501
511
|
: undefined;
|
|
@@ -521,11 +531,11 @@ swipeEnabled: _swipeEnabled, swipeThreshold: _swipeThreshold, velocityThreshold:
|
|
|
521
531
|
const hoverHandlers = Platform.OS === "web" && expandOnHover
|
|
522
532
|
? {
|
|
523
533
|
onMouseEnter: () => {
|
|
524
|
-
|
|
534
|
+
setHoverSuppressed(false);
|
|
525
535
|
setHovered(true);
|
|
526
536
|
},
|
|
527
537
|
onMouseLeave: () => {
|
|
528
|
-
|
|
538
|
+
setHoverSuppressed(false);
|
|
529
539
|
setHovered(false);
|
|
530
540
|
},
|
|
531
541
|
}
|
|
@@ -3,9 +3,10 @@ import * as React from "react";
|
|
|
3
3
|
import { Animated, Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
|
-
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
6
|
+
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { interaction } from "../constants/interaction.js";
|
|
9
10
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
11
|
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
|
11
12
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
@@ -76,7 +77,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
76
77
|
return (_jsx(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DropdownMenuPrimitive.Overlay, { style: Platform.select({
|
|
77
78
|
native: StyleSheet.absoluteFill,
|
|
78
79
|
default: undefined,
|
|
79
|
-
}), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill,
|
|
80
|
+
}), children: _jsx(AnimatedView, { type: "fade", style: [StyleSheet.absoluteFill, styles.passThrough], children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
|
|
80
81
|
backgroundColor: theme.colors.popover,
|
|
81
82
|
borderWidth: 1,
|
|
82
83
|
borderColor: theme.colors.border,
|
|
@@ -109,7 +110,7 @@ function DropdownMenuItem({ inset = false, variant = "default", style: styleOver
|
|
|
109
110
|
outlineStyle: "none",
|
|
110
111
|
userSelect: "none",
|
|
111
112
|
}),
|
|
112
|
-
...(props.disabled && { opacity:
|
|
113
|
+
...(props.disabled && { opacity: interaction.disabledOpacity }),
|
|
113
114
|
...(inset && { paddingLeft: spacing.xl }),
|
|
114
115
|
...(styleOverride && typeof styleOverride !== "function"
|
|
115
116
|
? StyleSheet.flatten(styleOverride)
|
|
@@ -130,7 +131,7 @@ function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props })
|
|
|
130
131
|
outlineStyle: "none",
|
|
131
132
|
userSelect: "none",
|
|
132
133
|
}),
|
|
133
|
-
...(props.disabled && { opacity:
|
|
134
|
+
...(props.disabled && { opacity: interaction.disabledOpacity }),
|
|
134
135
|
...(styleOverride && typeof styleOverride !== "function"
|
|
135
136
|
? StyleSheet.flatten(styleOverride)
|
|
136
137
|
: {}),
|
|
@@ -157,7 +158,7 @@ function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
|
157
158
|
outlineStyle: "none",
|
|
158
159
|
userSelect: "none",
|
|
159
160
|
}),
|
|
160
|
-
...(props.disabled && { opacity:
|
|
161
|
+
...(props.disabled && { opacity: interaction.disabledOpacity }),
|
|
161
162
|
...(styleOverride && typeof styleOverride !== "function"
|
|
162
163
|
? StyleSheet.flatten(styleOverride)
|
|
163
164
|
: {}),
|
|
@@ -172,7 +173,7 @@ function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
|
172
173
|
backgroundColor: theme.colors.text,
|
|
173
174
|
height: 8,
|
|
174
175
|
width: 8,
|
|
175
|
-
borderRadius:
|
|
176
|
+
borderRadius: spacing.radiusFull,
|
|
176
177
|
} }) }) }), typeof children === "function" ? null : children] }) }) }) }));
|
|
177
178
|
}
|
|
178
179
|
function DropdownMenuLabel({ inset = false, style: styleOverride, ...props }) {
|
|
@@ -216,7 +217,10 @@ function DropdownMenuShortcut({ children, text, style: styleOverride, ...props }
|
|
|
216
217
|
styleOverride,
|
|
217
218
|
], children: text ?? children }));
|
|
218
219
|
}
|
|
219
|
-
const styles = StyleSheet.create({
|
|
220
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
221
|
+
// Overlay layer: covers the screen without swallowing touches (style, not the
|
|
222
|
+
// deprecated `pointerEvents` prop).
|
|
223
|
+
passThrough: { pointerEvents: "box-none" },
|
|
220
224
|
item: {
|
|
221
225
|
position: "relative",
|
|
222
226
|
flexDirection: "row",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { StyleProp, ViewStyle } from "react-native";
|
|
3
|
-
import { type ButtonProps } from "./Button";
|
|
4
|
-
import { type IconName } from "./Icon";
|
|
3
|
+
import { type ButtonProps } from "./Button.js";
|
|
4
|
+
import { type IconName } from "./Icon.js";
|
|
5
5
|
export interface EmptyStateProps {
|
|
6
6
|
/** Icon name to display */
|
|
7
7
|
icon?: IconName;
|
|
@@ -4,6 +4,7 @@ import { SansSerifBoldText, SansSerifText } from "./StyledText.js";
|
|
|
4
4
|
import { Button } from "./Button.js";
|
|
5
5
|
import { Icon } from "./Icon.js";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
7
8
|
import { spacing } from "../constants/spacing.js";
|
|
8
9
|
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
10
|
/**
|
|
@@ -26,7 +27,9 @@ import { createThemedStyles } from "../lib/themedStyles.js";
|
|
|
26
27
|
export function EmptyState({ icon, iconSize = spacing.iconMd, title, description, actionLabel, onAction, actionPreset = "default", style, children, bordered = false, }) {
|
|
27
28
|
const { theme } = useTheme();
|
|
28
29
|
const styles = themedStyles(theme);
|
|
29
|
-
|
|
30
|
+
// Host-app `setShape({ card })` shapes the bordered frame like a Card.
|
|
31
|
+
const cardRadius = shapeRadius(useShape("card"));
|
|
32
|
+
return (_jsxs(View, { style: [styles.container, bordered && styles.bordered, bordered && cardRadius, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, size: "lg", style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, size: "base", style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
|
|
30
33
|
}
|
|
31
34
|
const MEDIA_SIZE = 56;
|
|
32
35
|
const createStyles = (theme) => StyleSheet.create({
|
|
@@ -74,4 +77,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
74
77
|
alignSelf: "center",
|
|
75
78
|
},
|
|
76
79
|
});
|
|
77
|
-
const themedStyles = createThemedStyles(createStyles);
|
|
80
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles(createStyles);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface HydratedProps {
|
|
3
|
+
/** What the server HTML and the hydration pass show in place of the children. @default null */
|
|
4
|
+
fallback?: ReactNode;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Hydrated
|
|
9
|
+
*
|
|
10
|
+
* Renders `fallback` on the server and through the hydration pass, then its
|
|
11
|
+
* children in the first client render — the gate for markup the server cannot
|
|
12
|
+
* reproduce. Under Expo Router's SSR the server renders the app inside
|
|
13
|
+
* `+html.tsx` while the client hydrates `#root`, so every `useId()` differs
|
|
14
|
+
* between the two trees; the kit defers its own ids, but a Radix-backed
|
|
15
|
+
* component (`Tabs`, `Accordion`, `Collapsible`, `Select`, `DropdownMenu`,
|
|
16
|
+
* `Popover`) emits ids on both sides and logs a hydration mismatch when it is
|
|
17
|
+
* part of a server-rendered route. Wrap that subtree, not the page: the rest of
|
|
18
|
+
* the screen keeps its server HTML. Give it a fallback that holds the layout
|
|
19
|
+
* (a placeholder the same height) so nothing shifts when the children mount.
|
|
20
|
+
*
|
|
21
|
+
* Do not use it for content that must be in the server HTML (a FAQ's questions,
|
|
22
|
+
* a product's description); accept the development-only log there instead. On
|
|
23
|
+
* native there is no hydration pass, so children render immediately.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Hydrated fallback={<View style={{ height: 240 }} />}>
|
|
27
|
+
* <Tabs defaultValue="day">…</Tabs>
|
|
28
|
+
* </Hydrated>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function Hydrated({ fallback, children }: HydratedProps): import("react").JSX.Element;
|