@mrmeg/expo-ui 0.27.1 → 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 +598 -0
- package/LLM_USAGE.md +233 -35
- package/README.md +504 -103
- 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 +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -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 +87 -16
- 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 +5 -2
- 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 +73 -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 +20 -30
- 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 +71 -26
- 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/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.js +12 -4
- 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 -5
- package/dist/lib/index.js +1 -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/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,12 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import { Modal, Platform, StyleSheet, View } from "react-native";
|
|
3
4
|
import * as DialogPrimitive from "@rn-primitives/dialog";
|
|
4
5
|
import * as AlertDialogPrimitive from "@rn-primitives/alert-dialog";
|
|
5
6
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
7
|
import { KeyboardAvoidingView } from "./KeyboardAvoidingView.js";
|
|
7
|
-
import {
|
|
8
|
+
import { useWindowInsets } from "../hooks/useWindowInsets.js";
|
|
9
|
+
import { useKeyboardDismissResponder } from "./keyboardDismiss.js";
|
|
10
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
8
11
|
import { StyledText } from "./StyledText.js";
|
|
9
12
|
import { useTheme } from "../hooks/useTheme.js";
|
|
13
|
+
import { useShape } from "../hooks/useShape.js";
|
|
10
14
|
import { spacing } from "../constants/spacing.js";
|
|
11
15
|
import { palette } from "../constants/colors.js";
|
|
12
16
|
// ============================================================================
|
|
@@ -64,24 +68,85 @@ function DialogPresentation({ Portal, open, portalHost, onRequestClose, children
|
|
|
64
68
|
/**
|
|
65
69
|
* Keyboard avoidance owner for dialog content.
|
|
66
70
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
73
81
|
* `DismissKeyboard` in dialog content adds no second layer. Do not wrap dialog
|
|
74
82
|
* content in another `KeyboardAvoidingView`.
|
|
75
83
|
*
|
|
76
|
-
*
|
|
77
|
-
* outside the root avoidance there, and a dialog is not keyboard-avoided yet.
|
|
84
|
+
* Web has no software keyboard and keeps the portal-host tree unchanged.
|
|
78
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
|
+
}
|
|
79
103
|
function DialogKeyboardAvoidance({ children }) {
|
|
80
|
-
if (Platform.OS
|
|
104
|
+
if (Platform.OS === "web") {
|
|
81
105
|
return _jsx(_Fragment, { children: children });
|
|
82
106
|
}
|
|
83
107
|
return (_jsx(KeyboardAvoidingView, { style: overlayStyles.fill, behavior: "padding", keyboardVerticalOffset: 0, children: children }));
|
|
84
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
|
+
}
|
|
85
150
|
// ============================================================================
|
|
86
151
|
// Dialog
|
|
87
152
|
// ============================================================================
|
|
@@ -97,7 +162,10 @@ function DialogRoot({ children, ...props }) {
|
|
|
97
162
|
}
|
|
98
163
|
function DialogContent({ portalHost, style, children, ...props }) {
|
|
99
164
|
const { theme, getShadowStyle, getContrastingColor } = useTheme();
|
|
165
|
+
const dialogRadius = useShape("dialog")?.borderRadius ?? spacing.radiusLg;
|
|
166
|
+
const safeAreaPadding = useDialogSafeAreaPadding();
|
|
100
167
|
const { open, onOpenChange } = DialogPrimitive.useRootContext();
|
|
168
|
+
const dismissBoundaryProps = useDialogKeyboardDismissBoundary(true);
|
|
101
169
|
const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
|
|
102
170
|
return (_jsx(DialogPresentation, { Portal: DialogPrimitive.Portal, open: open, portalHost: portalHost, onRequestClose: () => onOpenChange(false), children: _jsx(DialogPrimitive.Overlay
|
|
103
171
|
// On web the primitive wraps the overlay in a react-native-web
|
|
@@ -121,19 +189,19 @@ function DialogContent({ portalHost, style, children, ...props }) {
|
|
|
121
189
|
StyleSheet.absoluteFill,
|
|
122
190
|
{ backgroundColor: theme.colors.overlay },
|
|
123
191
|
Platform.OS === "web" && { zIndex: 50 },
|
|
124
|
-
]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, children: _jsx(DialogKeyboardAvoidance, { 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([
|
|
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([
|
|
125
193
|
{
|
|
126
194
|
backgroundColor: theme.colors.popover,
|
|
127
195
|
borderColor: theme.colors.border,
|
|
128
196
|
borderWidth: 1,
|
|
129
|
-
borderRadius:
|
|
197
|
+
borderRadius: dialogRadius,
|
|
130
198
|
padding: spacing.dialogPadding,
|
|
131
199
|
gap: spacing.md,
|
|
132
200
|
width: "100%",
|
|
133
201
|
...getShadowStyle("soft"),
|
|
134
202
|
},
|
|
135
203
|
style,
|
|
136
|
-
]), ...props, children: children }) }) }) }) }) }) }) }) }));
|
|
204
|
+
]), ...dismissBoundaryProps, ...props, children: children }) }) }) }) }) }) }) }) }));
|
|
137
205
|
}
|
|
138
206
|
function DialogHeader({ children, style, ...props }) {
|
|
139
207
|
return (_jsx(View, { style: StyleSheet.flatten([{ gap: spacing.xs }, style]), ...props, children: children }));
|
|
@@ -191,18 +259,21 @@ function AlertDialogRoot({ children, ...props }) {
|
|
|
191
259
|
}
|
|
192
260
|
function AlertDialogContent({ portalHost, style, children, ...props }) {
|
|
193
261
|
const { theme, getShadowStyle, getContrastingColor } = useTheme();
|
|
262
|
+
const dialogRadius = useShape("dialog")?.borderRadius ?? spacing.radiusLg;
|
|
263
|
+
const safeAreaPadding = useDialogSafeAreaPadding();
|
|
194
264
|
const { open, onOpenChange } = AlertDialogPrimitive.useRootContext();
|
|
265
|
+
const dismissBoundaryProps = useDialogKeyboardDismissBoundary();
|
|
195
266
|
const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
|
|
196
267
|
return (_jsx(DialogPresentation, { Portal: AlertDialogPrimitive.Portal, open: open, portalHost: portalHost, onRequestClose: () => onOpenChange(false), children: _jsx(AlertDialogPrimitive.Overlay, { style: StyleSheet.flatten([
|
|
197
268
|
StyleSheet.absoluteFill,
|
|
198
269
|
{ backgroundColor: theme.colors.overlay },
|
|
199
270
|
Platform.OS === "web" && { zIndex: 52 },
|
|
200
|
-
]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, children: _jsx(DialogKeyboardAvoidance, { 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(AlertDialogPrimitive.Content, { style: StyleSheet.flatten([
|
|
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([
|
|
201
272
|
{
|
|
202
273
|
backgroundColor: theme.colors.popover,
|
|
203
274
|
borderColor: theme.colors.border,
|
|
204
275
|
borderWidth: 1,
|
|
205
|
-
borderRadius:
|
|
276
|
+
borderRadius: dialogRadius,
|
|
206
277
|
padding: spacing.dialogPadding,
|
|
207
278
|
gap: spacing.md,
|
|
208
279
|
width: "100%",
|
|
@@ -247,7 +318,7 @@ const AlertDialog = Object.assign(AlertDialogRoot, {
|
|
|
247
318
|
// ============================================================================
|
|
248
319
|
// Shared styles
|
|
249
320
|
// ============================================================================
|
|
250
|
-
const overlayStyles = StyleSheet.create({
|
|
321
|
+
const overlayStyles = /*#__PURE__*/ StyleSheet.create({
|
|
251
322
|
// The keyboard-avoiding wrapper fills the overlay so its bottom padding
|
|
252
323
|
// shrinks the centered container instead of the card.
|
|
253
324
|
fill: {
|
|
@@ -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;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useHydrated } from "../hooks/useHydrated.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hydrated
|
|
5
|
+
*
|
|
6
|
+
* Renders `fallback` on the server and through the hydration pass, then its
|
|
7
|
+
* children in the first client render — the gate for markup the server cannot
|
|
8
|
+
* reproduce. Under Expo Router's SSR the server renders the app inside
|
|
9
|
+
* `+html.tsx` while the client hydrates `#root`, so every `useId()` differs
|
|
10
|
+
* between the two trees; the kit defers its own ids, but a Radix-backed
|
|
11
|
+
* component (`Tabs`, `Accordion`, `Collapsible`, `Select`, `DropdownMenu`,
|
|
12
|
+
* `Popover`) emits ids on both sides and logs a hydration mismatch when it is
|
|
13
|
+
* part of a server-rendered route. Wrap that subtree, not the page: the rest of
|
|
14
|
+
* the screen keeps its server HTML. Give it a fallback that holds the layout
|
|
15
|
+
* (a placeholder the same height) so nothing shifts when the children mount.
|
|
16
|
+
*
|
|
17
|
+
* Do not use it for content that must be in the server HTML (a FAQ's questions,
|
|
18
|
+
* a product's description); accept the development-only log there instead. On
|
|
19
|
+
* native there is no hydration pass, so children render immediately.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Hydrated fallback={<View style={{ height: 240 }} />}>
|
|
23
|
+
* <Tabs defaultValue="day">…</Tabs>
|
|
24
|
+
* </Hydrated>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function Hydrated({ fallback = null, children }) {
|
|
28
|
+
const hydrated = useHydrated();
|
|
29
|
+
return _jsx(_Fragment, { children: hydrated ? children : fallback });
|
|
30
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type
|
|
3
|
-
import type { ThemeColors } from "../constants/colors";
|
|
4
|
-
import { type IconName } from "./iconRegistry.generated";
|
|
2
|
+
import { type StyleProp, type TextStyle, type ViewProps, type ViewStyle } from "react-native";
|
|
3
|
+
import type { ThemeColors } from "../constants/colors.js";
|
|
4
|
+
import { type IconName } from "./iconRegistry.generated.js";
|
|
5
5
|
/**
|
|
6
6
|
* Kebab-case Lucide icon names the package bundles, served from
|
|
7
7
|
* `icon-names.json` through the generated registry. Add a name there and run
|
|
@@ -23,10 +23,22 @@ type IconBaseProps = {
|
|
|
23
23
|
color?: string | ThemeColorName;
|
|
24
24
|
/** Additional styles for positioning, transforms, etc. */
|
|
25
25
|
style?: StyleProp<TextStyle>;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Hides the icon from the accessibility tree. An icon with no
|
|
28
|
+
* `accessibilityLabel` is already hidden (see below), so this is only
|
|
29
|
+
* needed to silence a labelled icon. @default false
|
|
30
|
+
*/
|
|
27
31
|
decorative?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* What the icon means, for an icon that stands alone (no visible label next
|
|
34
|
+
* to it): `role="img"` + `aria-label` on web, `accessible` +
|
|
35
|
+
* `accessibilityLabel` on native. Omit it when the icon sits beside text
|
|
36
|
+
* that already says it — an unlabeled icon is hidden from assistive tech,
|
|
37
|
+
* which would otherwise announce an unnamed image.
|
|
38
|
+
*/
|
|
39
|
+
accessibilityLabel?: string;
|
|
28
40
|
};
|
|
29
|
-
type IconAccessibilityProps = Pick<ViewProps, "accessible" | "importantForAccessibility" | "accessibilityElementsHidden" | "aria-hidden">;
|
|
41
|
+
type IconAccessibilityProps = Pick<ViewProps, "accessible" | "importantForAccessibility" | "accessibilityElementsHidden" | "accessibilityLabel" | "aria-hidden" | "aria-label" | "role">;
|
|
30
42
|
/**
|
|
31
43
|
* Props handed to a `component`. Wide enough that any `lucide-react-native`
|
|
32
44
|
* component type-checks as-is (`LucideProps` extends `SvgProps`, whose `style`
|