@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.
Files changed (145) hide show
  1. package/CHANGELOG.md +712 -0
  2. package/LLM_USAGE.md +260 -32
  3. package/README.md +558 -101
  4. package/dist/components/Accordion.js +12 -6
  5. package/dist/components/AnimatedView.d.ts +1 -1
  6. package/dist/components/AnimatedView.js +5 -2
  7. package/dist/components/Avatar.d.ts +1 -1
  8. package/dist/components/Avatar.js +2 -2
  9. package/dist/components/Badge.js +5 -1
  10. package/dist/components/BottomSheet.d.ts +66 -9
  11. package/dist/components/BottomSheet.js +143 -39
  12. package/dist/components/Button.d.ts +20 -4
  13. package/dist/components/Button.js +64 -43
  14. package/dist/components/Card.d.ts +1 -1
  15. package/dist/components/Card.js +24 -4
  16. package/dist/components/Carousel.js +2 -2
  17. package/dist/components/Checkbox.js +51 -44
  18. package/dist/components/Collapsible.js +7 -3
  19. package/dist/components/Dialog.js +178 -29
  20. package/dist/components/Drawer.d.ts +1 -1
  21. package/dist/components/Drawer.js +65 -55
  22. package/dist/components/DropdownMenu.js +11 -7
  23. package/dist/components/EmptyState.d.ts +2 -2
  24. package/dist/components/EmptyState.js +5 -2
  25. package/dist/components/Hydrated.d.ts +31 -0
  26. package/dist/components/Hydrated.js +30 -0
  27. package/dist/components/Icon.d.ts +17 -5
  28. package/dist/components/Icon.js +31 -10
  29. package/dist/components/InputOTP.js +7 -3
  30. package/dist/components/Item.d.ts +103 -4
  31. package/dist/components/Item.js +168 -15
  32. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  33. package/dist/components/KeyboardAvoidingView.js +4 -1
  34. package/dist/components/Label.js +3 -2
  35. package/dist/components/MaxWidthContainer.js +1 -1
  36. package/dist/components/Notification.js +45 -27
  37. package/dist/components/Popover.d.ts +18 -5
  38. package/dist/components/Popover.js +72 -8
  39. package/dist/components/Progress.js +8 -6
  40. package/dist/components/RadioGroup.js +52 -49
  41. package/dist/components/Screen.d.ts +61 -0
  42. package/dist/components/Screen.js +62 -0
  43. package/dist/components/SectionHeader.d.ts +1 -1
  44. package/dist/components/SegmentedControl.d.ts +12 -4
  45. package/dist/components/SegmentedControl.js +94 -4
  46. package/dist/components/Select.js +21 -26
  47. package/dist/components/Skeleton.js +24 -10
  48. package/dist/components/Slider.d.ts +11 -3
  49. package/dist/components/Slider.js +74 -6
  50. package/dist/components/StatCard.d.ts +4 -2
  51. package/dist/components/StatCard.js +3 -1
  52. package/dist/components/StyledText.d.ts +7 -1
  53. package/dist/components/StyledText.js +5 -3
  54. package/dist/components/Switch.d.ts +1 -1
  55. package/dist/components/Switch.js +23 -32
  56. package/dist/components/Tabs.d.ts +2 -2
  57. package/dist/components/Tabs.js +13 -24
  58. package/dist/components/TextInput.d.ts +1 -1
  59. package/dist/components/TextInput.js +73 -27
  60. package/dist/components/Toggle.d.ts +2 -42
  61. package/dist/components/Toggle.js +38 -27
  62. package/dist/components/ToggleGroup.d.ts +1 -1
  63. package/dist/components/ToggleGroup.js +37 -32
  64. package/dist/components/Tooltip.d.ts +1 -1
  65. package/dist/components/Tooltip.js +9 -3
  66. package/dist/components/UIProvider.d.ts +22 -2
  67. package/dist/components/UIProvider.js +15 -3
  68. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  69. package/dist/components/bottomSheetDismiss.js +44 -0
  70. package/dist/components/bottomSheetHostSupport.d.ts +46 -0
  71. package/dist/components/bottomSheetHostSupport.js +67 -0
  72. package/dist/components/index.d.ts +46 -44
  73. package/dist/components/index.js +2 -0
  74. package/dist/components/keyboardDismiss.d.ts +25 -0
  75. package/dist/components/keyboardDismiss.js +60 -3
  76. package/dist/components/nativeTextField.android.js +13 -9
  77. package/dist/constants/colors.d.ts +40 -5
  78. package/dist/constants/colors.js +26 -8
  79. package/dist/constants/fonts.d.ts +41 -2
  80. package/dist/constants/fonts.js +46 -7
  81. package/dist/constants/index.d.ts +5 -4
  82. package/dist/constants/index.js +1 -0
  83. package/dist/constants/interaction.d.ts +18 -0
  84. package/dist/constants/interaction.js +18 -0
  85. package/dist/constants/spacing.d.ts +2 -1
  86. package/dist/constants/spacing.js +2 -1
  87. package/dist/design-system.json +154 -2
  88. package/dist/hooks/index.d.ts +12 -7
  89. package/dist/hooks/index.js +5 -0
  90. package/dist/hooks/useDimensions.d.ts +13 -8
  91. package/dist/hooks/useDimensions.js +123 -48
  92. package/dist/hooks/useFocusVisible.d.ts +30 -0
  93. package/dist/hooks/useFocusVisible.js +45 -0
  94. package/dist/hooks/useFontStyle.d.ts +2 -2
  95. package/dist/hooks/useFontStyle.js +3 -2
  96. package/dist/hooks/useHydrated.d.ts +13 -0
  97. package/dist/hooks/useHydrated.js +19 -0
  98. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  99. package/dist/hooks/useNotificationOffset.js +34 -0
  100. package/dist/hooks/useResources.d.ts +21 -20
  101. package/dist/hooks/useResources.js +72 -68
  102. package/dist/hooks/useScalePress.d.ts +8 -5
  103. package/dist/hooks/useScalePress.js +16 -9
  104. package/dist/hooks/useShape.d.ts +24 -0
  105. package/dist/hooks/useShape.js +24 -0
  106. package/dist/hooks/useStaggeredEntrance.js +9 -7
  107. package/dist/hooks/useTheme.d.ts +3 -3
  108. package/dist/hooks/useTheme.js +177 -66
  109. package/dist/hooks/useWindowInsets.d.ts +13 -0
  110. package/dist/hooks/useWindowInsets.js +23 -0
  111. package/dist/index.d.ts +5 -5
  112. package/dist/lib/composeRefs.d.ts +8 -0
  113. package/dist/lib/composeRefs.js +20 -0
  114. package/dist/lib/haptics.d.ts +12 -0
  115. package/dist/lib/haptics.js +21 -0
  116. package/dist/lib/index.d.ts +6 -4
  117. package/dist/lib/index.js +2 -0
  118. package/dist/lib/interFonts.d.ts +15 -0
  119. package/dist/lib/interFonts.js +1 -0
  120. package/dist/lib/interFonts.native.d.ts +3 -0
  121. package/dist/lib/interFonts.native.js +20 -0
  122. package/dist/lib/popoverPlacement.d.ts +36 -0
  123. package/dist/lib/popoverPlacement.js +29 -0
  124. package/dist/lib/stateSurface.d.ts +28 -0
  125. package/dist/lib/stateSurface.js +30 -0
  126. package/dist/lib/themedStyles.d.ts +1 -1
  127. package/dist/lib/useAnimatedValue.d.ts +15 -0
  128. package/dist/lib/useAnimatedValue.js +19 -0
  129. package/dist/lib/webHitTarget.d.ts +40 -0
  130. package/dist/lib/webHitTarget.js +37 -0
  131. package/dist/state/feedbackStore.d.ts +24 -0
  132. package/dist/state/feedbackStore.js +16 -0
  133. package/dist/state/globalUIStore.d.ts +27 -0
  134. package/dist/state/globalUIStore.js +23 -0
  135. package/dist/state/index.d.ts +7 -5
  136. package/dist/state/index.js +2 -0
  137. package/dist/state/initialScheme.d.ts +27 -0
  138. package/dist/state/initialScheme.js +29 -0
  139. package/dist/state/notify.d.ts +1 -1
  140. package/dist/state/themeColorScope.d.ts +1 -1
  141. package/dist/state/themeStore.d.ts +69 -16
  142. package/dist/state/themeStore.js +65 -38
  143. package/llms-full.md +56 -8
  144. package/llms.txt +10 -1
  145. package/package.json +69 -2
@@ -1,10 +1,15 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { useState } from "react";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
3
  import { Icon } from "./Icon.js";
4
- import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
4
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
5
5
  import { useTheme } from "../hooks/useTheme.js";
6
+ import { useFocusVisible } from "../hooks/useFocusVisible.js";
6
7
  import { spacing } from "../constants/spacing.js";
8
+ import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
9
+ import { interaction } from "../constants/interaction.js";
10
+ import { hapticSelection } from "../lib/haptics.js";
7
11
  import { useScalePress } from "../hooks/useScalePress.js";
12
+ import { useItemControlLabel } from "./Item.js";
8
13
  import * as TogglePrimitive from "@rn-primitives/toggle";
9
14
  import { Platform, StyleSheet, ActivityIndicator, Animated } from "react-native";
10
15
  import { palette } from "../constants/colors.js";
@@ -73,11 +78,12 @@ const TOGGLE_SIZES = {
73
78
  * </Toggle>
74
79
  * ```
75
80
  */
76
- function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, ...props }) {
81
+ // Dev-only, once per session: the same unnamed toggle re-renders constantly.
82
+ let warnedIconOnly = false;
83
+ function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, onPressedChange, ...props }) {
77
84
  const { theme, getContrastingColor, getFocusRingStyle, withAlpha } = useTheme();
78
85
  const sizeConfig = TOGGLE_SIZES[size];
79
86
  const focusRingStyle = getFocusRingStyle();
80
- const [focused, setFocused] = useState(false);
81
87
  // Calculate text color based on state and variant
82
88
  const getTextColor = () => {
83
89
  if (props.pressed) {
@@ -106,28 +112,33 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
106
112
  const children = props.children;
107
113
  const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
108
114
  disabled: !!isDisabled,
109
- scaleTo: 0.92,
115
+ scaleTo: interaction.controlPressedScale,
110
116
  haptic: false,
111
117
  });
112
- const showFocusRing = (event) => {
113
- let ringVisible = true;
114
- if (Platform.OS === "web") {
115
- const target = event?.nativeEvent?.target;
116
- if (target && typeof target.matches === "function") {
117
- try {
118
- ringVisible = target.matches(":focus-visible");
119
- }
120
- catch {
121
- ringVisible = true;
122
- }
123
- }
124
- }
125
- setFocused(ringVisible);
126
- };
127
- const hideFocusRing = () => {
128
- setFocused(false);
118
+ // A toggle is a selection control: its state change is the haptic moment,
119
+ // not the press, so it follows the `"selection"` setting like Switch does.
120
+ const handlePressedChange = (next) => {
121
+ hapticSelection();
122
+ onPressedChange?.(next);
129
123
  };
130
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...props, disabled: isDisabled, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
124
+ const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
125
+ const rowLabel = useItemControlLabel(props);
126
+ if (process.env.NODE_ENV !== "production" &&
127
+ iconOnly &&
128
+ !warnedIconOnly &&
129
+ props.accessibilityLabel === undefined &&
130
+ props["aria-label"] === undefined &&
131
+ props["aria-labelledby"] === undefined &&
132
+ rowLabel.accessibilityLabel === undefined &&
133
+ rowLabel["aria-labelledby"] === undefined) {
134
+ warnedIconOnly = true;
135
+ console.warn("Toggle: an icon-only toggle (`iconOnly`) has no accessible name. Pass `accessibilityLabel` " +
136
+ "(what the toggle turns on, e.g. \"Bold\") so screen readers announce more than \"button\".");
137
+ }
138
+ // Web pointer target: 44pt tall through a transparent child inside the drawn
139
+ // box (react-native-web ignores `hitSlop`); native keeps DEFAULT_HIT_SLOP.
140
+ const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.root.borderWidth }));
141
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...rowLabel, ...props, disabled: isDisabled, onPressedChange: handlePressedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
131
142
  ...styles.root,
132
143
  height: sizeConfig.height,
133
144
  minWidth: sizeConfig.minWidth,
@@ -152,7 +163,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
152
163
  borderColor: theme.colors.primary,
153
164
  }),
154
165
  // Disabled state
155
- opacity: isDisabled ? 0.5 : 1,
166
+ opacity: isDisabled ? interaction.disabledOpacity : 1,
156
167
  // Web-specific styles
157
168
  ...(Platform.OS === "web" && {
158
169
  cursor: isDisabled ? "not-allowed" : "pointer",
@@ -166,13 +177,13 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
166
177
  selected: props.pressed,
167
178
  disabled: !!isDisabled,
168
179
  busy: loading,
169
- }, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
180
+ }, children: loading ? (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) }), hitTarget] })) : typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
170
181
  }
171
182
  function ToggleIcon({ name, size, color }) {
172
183
  const contextColor = React.use(TextColorContext);
173
184
  return _jsx(Icon, { name: name, size: size || spacing.iconMd, color: color || contextColor });
174
185
  }
175
- const styles = StyleSheet.create({
186
+ const styles = /*#__PURE__*/ StyleSheet.create({
176
187
  root: {
177
188
  flexDirection: "row",
178
189
  alignItems: "center",
@@ -1,4 +1,4 @@
1
- import type { IconName } from "./Icon";
1
+ import type { IconName } from "./Icon.js";
2
2
  import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
3
3
  import * as React from "react";
4
4
  type ToggleGroupVariant = "default" | "outline";
@@ -1,8 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Icon } from "./Icon.js";
3
- import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
3
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
4
4
  import { spacing } from "../constants/spacing.js";
5
+ import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
6
+ import { interaction } from "../constants/interaction.js";
7
+ import { hapticSelection } from "../lib/haptics.js";
5
8
  import { useTheme } from "../hooks/useTheme.js";
9
+ import { useFocusVisible } from "../hooks/useFocusVisible.js";
6
10
  import { useScalePress } from "../hooks/useScalePress.js";
7
11
  import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
8
12
  import * as React from "react";
@@ -67,17 +71,26 @@ function useToggleGroupContext() {
67
71
  function ToggleGroup({ variant = "default", size = "default", children, ...props }) {
68
72
  const { getShadowStyle } = useTheme();
69
73
  const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]);
74
+ // The primitive reports a change once per user selection (single or
75
+ // multiple), which is the selection-haptic moment; forward it unchanged.
76
+ const { onValueChange } = props;
77
+ const handleValueChange = React.useCallback((value) => {
78
+ hapticSelection();
79
+ onValueChange?.(value);
80
+ }, [onValueChange]);
70
81
  // Count valid children for first/last detection
71
82
  const childrenArray = React.Children.toArray(children);
72
83
  const validChildren = childrenArray.filter((child) => React.isValidElement(child) && child.type === ToggleGroupItem);
73
84
  const childCount = validChildren.length;
74
- // Clone children with position props
75
- let itemIndex = 0;
85
+ // Clone children with position props. The running item index lives on an
86
+ // object: the React Compiler can't compile a reassigned `let` captured by
87
+ // the callback, and skipped the component.
88
+ const position = { itemIndex: 0 };
76
89
  const enhancedChildren = React.Children.map(children, (child) => {
77
90
  if (React.isValidElement(child) && child.type === ToggleGroupItem) {
78
- const isFirst = itemIndex === 0;
79
- const isLast = itemIndex === childCount - 1;
80
- itemIndex++;
91
+ const isFirst = position.itemIndex === 0;
92
+ const isLast = position.itemIndex === childCount - 1;
93
+ position.itemIndex += 1;
81
94
  return React.cloneElement(child, {
82
95
  isFirst,
83
96
  isLast,
@@ -85,7 +98,12 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
85
98
  }
86
99
  return child;
87
100
  });
88
- return (_jsx(ToggleGroupPrimitive.Root, { ...props, style: {
101
+ // The web primitive's Root/Item render generic roles (Radix's `group`, then
102
+ // `role='button'` on every item) while Radix still emits `aria-checked` for a
103
+ // single-select item — invalid on a button. Name the roles the way native
104
+ // already does: a radiogroup of radios, or a group of (aria-pressed) buttons.
105
+ const webRoles = Platform.OS === "web" ? { role: props.type === "single" ? "radiogroup" : "group" } : undefined;
106
+ return (_jsx(ToggleGroupPrimitive.Root, { ...props, ...webRoles, onValueChange: handleValueChange, style: {
89
107
  flexDirection: "row",
90
108
  alignItems: "center",
91
109
  borderRadius: spacing.radiusMd,
@@ -104,33 +122,17 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
104
122
  function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }) {
105
123
  const { theme, withAlpha, getContrastingColor, getFocusRingStyle } = useTheme();
106
124
  const context = useToggleGroupContext();
107
- const { value: groupValue } = ToggleGroupPrimitive.useRootContext();
125
+ const { value: groupValue, type: groupType } = ToggleGroupPrimitive.useRootContext();
126
+ // See ToggleGroup: give the web item the role its aria state is valid on.
127
+ const webRole = Platform.OS === "web" ? { role: groupType === "single" ? "radio" : "button" } : undefined;
108
128
  const sizeConfig = TOGGLE_GROUP_SIZES[context.size];
109
129
  const focusRingStyle = getFocusRingStyle();
110
- const [focused, setFocused] = React.useState(false);
111
130
  const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
112
131
  disabled: !!props.disabled,
113
- scaleTo: 0.97,
132
+ scaleTo: interaction.pressedScale,
114
133
  haptic: false,
115
134
  });
116
- const showFocusRing = (event) => {
117
- let ringVisible = true;
118
- if (Platform.OS === "web") {
119
- const target = event?.nativeEvent?.target;
120
- if (target && typeof target.matches === "function") {
121
- try {
122
- ringVisible = target.matches(":focus-visible");
123
- }
124
- catch {
125
- ringVisible = true;
126
- }
127
- }
128
- }
129
- setFocused(ringVisible);
130
- };
131
- const hideFocusRing = () => {
132
- setFocused(false);
133
- };
135
+ const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
134
136
  // Check if this item is selected
135
137
  const isSelected = ToggleGroupPrimitive.utils.getIsSelected(groupValue, props.value);
136
138
  // Determine the actual background color for this item
@@ -145,7 +147,10 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
145
147
  const textColor = isSelected
146
148
  ? getContrastingColor(itemBgColor, theme.colors.foreground, theme.colors.background)
147
149
  : theme.colors.foreground;
148
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(ToggleGroupPrimitive.Item, { ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
150
+ // Web pointer target, as in Toggle: 44pt tall through a transparent child
151
+ // inside the drawn box; native keeps DEFAULT_HIT_SLOP.
152
+ const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.item.borderWidth }));
153
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(ToggleGroupPrimitive.Item, { ...props, ...webRole, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
149
154
  ...styles.item,
150
155
  height: sizeConfig.height,
151
156
  minWidth: sizeConfig.minWidth,
@@ -199,13 +204,13 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
199
204
  }),
200
205
  }),
201
206
  ...(focused && !props.disabled ? focusRingStyle : null),
202
- }, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
207
+ }, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
203
208
  }
204
209
  function ToggleGroupIcon({ name, size, color }) {
205
210
  const contextColor = React.use(TextColorContext);
206
211
  return _jsx(Icon, { name: name, size: size || spacing.iconMd, color: color || contextColor });
207
212
  }
208
- const styles = StyleSheet.create({
213
+ const styles = /*#__PURE__*/ StyleSheet.create({
209
214
  item: {
210
215
  flexDirection: "row",
211
216
  alignItems: "center",
@@ -73,7 +73,7 @@ interface TooltipProps extends TooltipPrimitive.RootProps {
73
73
  * // With positioning
74
74
  * <Tooltip>
75
75
  * <TooltipTrigger asChild>
76
- * <Icon as={Info} size={20} />
76
+ * <Icon name="info" size={20} />
77
77
  * </TooltipTrigger>
78
78
  * <TooltipContent side="bottom" align="start">
79
79
  * <StyledText>Information tooltip</StyledText>
@@ -2,12 +2,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Platform, StyleSheet, View } from "react-native";
4
4
  import { AnimatedView } from "./AnimatedView.js";
5
- import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
5
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
7
  import { spacing } from "../constants/spacing.js";
8
8
  import * as TooltipPrimitive from "@rn-primitives/tooltip";
9
9
  import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
10
10
  import { palette } from "../constants/colors.js";
11
+ // The overlay layer covers the screen but must not swallow touches around the
12
+ // content; a style, not the deprecated `pointerEvents` prop (web warns and
13
+ // lazily loads its warning module mid-render).
14
+ const styles = /*#__PURE__*/ StyleSheet.create({
15
+ passThrough: { pointerEvents: "box-none" },
16
+ });
11
17
  /**
12
18
  * Tooltip Trigger Component
13
19
  * The element that triggers the tooltip to appear on hover (web) or press (native)
@@ -71,7 +77,7 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
71
77
  ...getShadowStyle("soft"),
72
78
  },
73
79
  ]) ?? undefined;
74
- return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
80
+ return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, style: [StyleSheet.absoluteFill, styles.passThrough], children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
75
81
  }
76
82
  function TooltipBody({ children, style, ...props }) {
77
83
  return (_jsx(View, { style: [
@@ -101,7 +107,7 @@ function TooltipBody({ children, style, ...props }) {
101
107
  * // With positioning
102
108
  * <Tooltip>
103
109
  * <TooltipTrigger asChild>
104
- * <Icon as={Info} size={20} />
110
+ * <Icon name="info" size={20} />
105
111
  * </TooltipTrigger>
106
112
  * <TooltipContent side="bottom" align="start">
107
113
  * <StyledText>Information tooltip</StyledText>
@@ -1,5 +1,7 @@
1
1
  import * as React from "react";
2
- import { type KeyboardAvoidingViewProps } from "./KeyboardAvoidingView";
2
+ import { type HapticsSetting } from "../state/feedbackStore.js";
3
+ import type { ResolvedTheme } from "../state/themeStore.js";
4
+ import { type KeyboardAvoidingViewProps } from "./KeyboardAvoidingView.js";
3
5
  export interface UIProviderProps {
4
6
  children: React.ReactNode;
5
7
  /**
@@ -30,5 +32,23 @@ export interface UIProviderProps {
30
32
  * Props forwarded to the keyboard-avoiding root when enabled.
31
33
  */
32
34
  keyboardAvoidingProps?: Omit<KeyboardAvoidingViewProps, "children">;
35
+ /**
36
+ * Which interactions may vibrate on native: `"off"`, `"selection"` (state
37
+ * changes on Switch, Checkbox, Toggle, ToggleGroup, SegmentedControl) or
38
+ * `"all"` (selection plus a light tap on press for Button, pressable Card
39
+ * and Item). Written to the feedback store on mount and whenever it changes;
40
+ * omit it to leave the store as it is. Web never vibrates.
41
+ *
42
+ * @default "selection"
43
+ */
44
+ haptics?: HapticsSetting;
45
+ /**
46
+ * The scheme the first render should use before the persisted preference
47
+ * has been read (a web SSR hint — see `InitialSchemeProvider` in
48
+ * `@mrmeg/expo-ui/state`). Only reaches `useTheme()` calls BELOW this
49
+ * provider; a root layout that reads the theme above it should wrap itself
50
+ * in `InitialSchemeProvider` instead. Native ignores it.
51
+ */
52
+ initialScheme?: ResolvedTheme;
33
53
  }
34
- export declare function UIProvider({ children, notification, portalHost, statusBar, keyboardAvoiding, keyboardAvoidingProps, }: UIProviderProps): React.JSX.Element;
54
+ export declare function UIProvider({ children, notification, portalHost, statusBar, keyboardAvoiding: keyboardAvoidingProp, keyboardAvoidingProps, haptics, initialScheme, }: UIProviderProps): React.JSX.Element;
@@ -1,11 +1,23 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
2
3
  import { Platform } from "react-native";
4
+ import { useFeedbackStore } from "../state/feedbackStore.js";
5
+ import { InitialSchemeProvider } from "../state/initialScheme.js";
3
6
  import { PortalHost } from "@rn-primitives/portal";
4
7
  import { Notification } from "./Notification.js";
5
8
  import { StatusBar } from "./StatusBar.js";
6
9
  import { KeyboardAvoidingView, } from "./KeyboardAvoidingView.js";
7
- export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, keyboardAvoiding = Platform.OS !== "web", keyboardAvoidingProps, }) {
10
+ export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, keyboardAvoiding: keyboardAvoidingProp, keyboardAvoidingProps, haptics, initialScheme, }) {
11
+ // Controls read the setting at event time (`hapticPress` / `hapticSelection`),
12
+ // so an effect is early enough; no child renders differently because of it.
13
+ React.useEffect(() => {
14
+ if (haptics !== undefined)
15
+ useFeedbackStore.getState().setHaptics(haptics);
16
+ }, [haptics]);
17
+ // Resolved in the body rather than as a default parameter: the React
18
+ // Compiler can't reorder a computed default, and skipped the component.
19
+ const keyboardAvoiding = keyboardAvoidingProp === undefined ? Platform.OS !== "web" : keyboardAvoidingProp;
8
20
  const { style: keyboardAvoidingStyle, ...restKeyboardAvoidingProps } = keyboardAvoidingProps ?? {};
9
21
  const content = keyboardAvoiding ? (_jsx(KeyboardAvoidingView, { style: [{ flex: 1 }, keyboardAvoidingStyle], ...restKeyboardAvoidingProps, children: children })) : (children);
10
- return (_jsxs(_Fragment, { children: [content, notification ? _jsx(Notification, {}) : null, portalHost ? _jsx(PortalHost, {}) : null, statusBar ? _jsx(StatusBar, {}) : null] }));
22
+ return (_jsxs(InitialSchemeProvider, { scheme: initialScheme, children: [content, notification ? _jsx(Notification, {}) : null, portalHost ? _jsx(PortalHost, {}) : null, statusBar ? _jsx(StatusBar, {}) : null] }));
11
23
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dismiss-complete detection for `BottomSheet.onDismissed` on web.
3
+ *
4
+ * `@expo/ui`'s web sheet is an HTML `<dialog>`. When the sheet's `index` goes
5
+ * to -1 it fires `onClose` synchronously, then plays its exit animation and
6
+ * calls `dialog.close()` on `animationend` (a 400 ms fallback timer covers a
7
+ * missed event; `prefers-reduced-motion` closes at once). The hosted RN column
8
+ * is a descendant of that element and stays mounted through the exit, so the
9
+ * element's standard `close` event is the moment the sheet is gone. iOS and
10
+ * Android need none of this: there `@expo/ui`'s close callback is already
11
+ * post-dismissal (see `BottomSheet.tsx`).
12
+ */
13
+ /**
14
+ * Call `callback` once the `<dialog>` enclosing `node` has closed. Fires on the
15
+ * same tick when there is no enclosing dialog (or no DOM node) or the dialog
16
+ * is already closed. Returns a cancel function that removes the pending
17
+ * listener without firing.
18
+ */
19
+ export declare function awaitDialogClose(node: unknown, callback: () => void): () => void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Dismiss-complete detection for `BottomSheet.onDismissed` on web.
3
+ *
4
+ * `@expo/ui`'s web sheet is an HTML `<dialog>`. When the sheet's `index` goes
5
+ * to -1 it fires `onClose` synchronously, then plays its exit animation and
6
+ * calls `dialog.close()` on `animationend` (a 400 ms fallback timer covers a
7
+ * missed event; `prefers-reduced-motion` closes at once). The hosted RN column
8
+ * is a descendant of that element and stays mounted through the exit, so the
9
+ * element's standard `close` event is the moment the sheet is gone. iOS and
10
+ * Android need none of this: there `@expo/ui`'s close callback is already
11
+ * post-dismissal (see `BottomSheet.tsx`).
12
+ */
13
+ function findEnclosingDialog(node) {
14
+ const closest = node
15
+ ?.closest;
16
+ if (typeof closest !== "function")
17
+ return null;
18
+ const dialog = closest.call(node, "dialog");
19
+ if (!dialog ||
20
+ typeof dialog.addEventListener !== "function" ||
21
+ typeof dialog.removeEventListener !== "function") {
22
+ return null;
23
+ }
24
+ return dialog;
25
+ }
26
+ /**
27
+ * Call `callback` once the `<dialog>` enclosing `node` has closed. Fires on the
28
+ * same tick when there is no enclosing dialog (or no DOM node) or the dialog
29
+ * is already closed. Returns a cancel function that removes the pending
30
+ * listener without firing.
31
+ */
32
+ export function awaitDialogClose(node, callback) {
33
+ const dialog = findEnclosingDialog(node);
34
+ if (!dialog || !dialog.open) {
35
+ callback();
36
+ return () => { };
37
+ }
38
+ const onClose = () => {
39
+ dialog.removeEventListener("close", onClose);
40
+ callback();
41
+ };
42
+ dialog.addEventListener("close", onClose);
43
+ return () => dialog.removeEventListener("close", onClose);
44
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Android `BottomSheet` keyboard avoidance is platform-owned, and the part the
3
+ * package depends on lives in the host: Material3's `ModalBottomSheet` shrinks
4
+ * the sheet with `imePadding()`, `@expo/ui`'s `RNHostView` re-reports its
5
+ * Compose size to the shadow tree (`onSizeChanged` → `ShadowNodeProxy.setViewSize`),
6
+ * and the `flexGrow: 1, height: 0` column the host wraps our children in
7
+ * follows. `expo-modules-core` <= 57.0.3 flushed that report only from a
8
+ * one-shot pre-draw listener on the activity window, which does not draw while
9
+ * the sheet's own dialog window animates the IME, so the column kept its
10
+ * detent height and `Footer` sat under the keyboard until the activity redrew
11
+ * (expo/expo#47778, fixed by expo/expo#47810 in 57.0.4 and 58.0.0; no 56.x
12
+ * release has it). Nothing in JS can observe a size the shadow tree never
13
+ * received, so the package names the floor instead: once per session, in dev,
14
+ * on Android, when the *native* core compiled into the app is older.
15
+ */
16
+ export interface ExpoModulesCoreVersion {
17
+ version: string;
18
+ major: number;
19
+ minor: number;
20
+ patch: number;
21
+ }
22
+ /** First `expo-modules-core` release whose `RNHostView` size flush always lands. */
23
+ export declare const MIN_EXPO_MODULES_CORE: Readonly<Omit<ExpoModulesCoreVersion, "version">>;
24
+ /**
25
+ * The native `expo-modules-core` version compiled into the running app, as the
26
+ * core's own `CoreModule` publishes it on `globalThis.expo` (Android and iOS,
27
+ * present on every Expo release the package supports, 56 through 58).
28
+ * `undefined` on web, in Jest, or when the core is not loaded.
29
+ */
30
+ export declare function readExpoModulesCoreVersion(): ExpoModulesCoreVersion | undefined;
31
+ /**
32
+ * Whether this core drops the sheet host's size update when the keyboard opens:
33
+ * a defined version lexicographically below {@link MIN_EXPO_MODULES_CORE}.
34
+ * An unknown version is not reported as broken.
35
+ */
36
+ export declare function bottomSheetHostDropsResize(version: ExpoModulesCoreVersion | undefined): boolean;
37
+ export declare function bottomSheetHostWarning(version: ExpoModulesCoreVersion): string;
38
+ /**
39
+ * Warn once per JS session, in development, on Android, when the native
40
+ * `expo-modules-core` is older than {@link MIN_EXPO_MODULES_CORE}. Called when a
41
+ * `BottomSheet.Content` mounts. Silent on iOS and web, where the sheet's
42
+ * avoidance does not go through `RNHostView`'s size state.
43
+ */
44
+ export declare function warnIfBottomSheetHostDropsResize(): void;
45
+ /** Test hook: forget that the warning was already shown. */
46
+ export declare function resetBottomSheetHostWarningForTests(): void;
@@ -0,0 +1,67 @@
1
+ import { Platform } from "react-native";
2
+ /** First `expo-modules-core` release whose `RNHostView` size flush always lands. */
3
+ export const MIN_EXPO_MODULES_CORE = {
4
+ major: 57,
5
+ minor: 0,
6
+ patch: 4,
7
+ };
8
+ /**
9
+ * The native `expo-modules-core` version compiled into the running app, as the
10
+ * core's own `CoreModule` publishes it on `globalThis.expo` (Android and iOS,
11
+ * present on every Expo release the package supports, 56 through 58).
12
+ * `undefined` on web, in Jest, or when the core is not loaded.
13
+ */
14
+ export function readExpoModulesCoreVersion() {
15
+ const expo = globalThis.expo;
16
+ const candidate = expo?.expoModulesCoreVersion;
17
+ if (typeof candidate !== "object" ||
18
+ candidate === null ||
19
+ typeof candidate.major !== "number" ||
20
+ typeof candidate.minor !== "number" ||
21
+ typeof candidate.patch !== "number") {
22
+ return undefined;
23
+ }
24
+ return candidate;
25
+ }
26
+ /**
27
+ * Whether this core drops the sheet host's size update when the keyboard opens:
28
+ * a defined version lexicographically below {@link MIN_EXPO_MODULES_CORE}.
29
+ * An unknown version is not reported as broken.
30
+ */
31
+ export function bottomSheetHostDropsResize(version) {
32
+ if (!version)
33
+ return false;
34
+ const { major, minor, patch } = MIN_EXPO_MODULES_CORE;
35
+ if (version.major !== major)
36
+ return version.major < major;
37
+ if (version.minor !== minor)
38
+ return version.minor < minor;
39
+ return version.patch < patch;
40
+ }
41
+ export function bottomSheetHostWarning(version) {
42
+ const { major, minor, patch } = MIN_EXPO_MODULES_CORE;
43
+ return (`@mrmeg/expo-ui BottomSheet: expo-modules-core ${version.version} drops the sheet's ` +
44
+ "size update when the keyboard opens on Android, so BottomSheet.Footer and the tail " +
45
+ "of Body end up under the keyboard (expo/expo#47778). Update expo-modules-core to " +
46
+ `>= ${major}.${minor}.${patch} (fixed in 57.0.4 / 58.0.0) and rebuild the app.`);
47
+ }
48
+ let warned = false;
49
+ /**
50
+ * Warn once per JS session, in development, on Android, when the native
51
+ * `expo-modules-core` is older than {@link MIN_EXPO_MODULES_CORE}. Called when a
52
+ * `BottomSheet.Content` mounts. Silent on iOS and web, where the sheet's
53
+ * avoidance does not go through `RNHostView`'s size state.
54
+ */
55
+ export function warnIfBottomSheetHostDropsResize() {
56
+ if (warned || !__DEV__ || Platform.OS !== "android")
57
+ return;
58
+ const version = readExpoModulesCoreVersion();
59
+ if (!version || !bottomSheetHostDropsResize(version))
60
+ return;
61
+ warned = true;
62
+ console.warn(bottomSheetHostWarning(version));
63
+ }
64
+ /** Test hook: forget that the warning was already shown. */
65
+ export function resetBottomSheetHostWarningForTests() {
66
+ warned = false;
67
+ }
@@ -1,44 +1,46 @@
1
- export * from "./Accordion";
2
- export * from "./Alert";
3
- export * from "./AnimatedView";
4
- export * from "./Avatar";
5
- export * from "./Badge";
6
- export * from "./BottomSheet";
7
- export * from "./Button";
8
- export * from "./Card";
9
- export * from "./Carousel";
10
- export * from "./Checkbox";
11
- export * from "./Collapsible";
12
- export * from "./Dialog";
13
- export * from "./DismissKeyboard";
14
- export * from "./keyboardDismiss";
15
- export * from "./Drawer";
16
- export * from "./DropdownMenu";
17
- export * from "./EmptyState";
18
- export * from "./ErrorBoundary";
19
- export * from "./Icon";
20
- export * from "./InputOTP";
21
- export * from "./Item";
22
- export * from "./KeyboardAvoidingView";
23
- export * from "./Label";
24
- export * from "./MaxWidthContainer";
25
- export * from "./Notification";
26
- export * from "./Popover";
27
- export * from "./Progress";
28
- export * from "./RadioGroup";
29
- export * from "./SectionHeader";
30
- export * from "./SegmentedControl";
31
- export * from "./Select";
32
- export * from "./Separator";
33
- export * from "./Skeleton";
34
- export * from "./Slider";
35
- export * from "./StatCard";
36
- export * from "./StatusBar";
37
- export * from "./StyledText";
38
- export * from "./Switch";
39
- export * from "./Tabs";
40
- export * from "./TextInput";
41
- export * from "./Toggle";
42
- export * from "./ToggleGroup";
43
- export * from "./Tooltip";
44
- export * from "./UIProvider";
1
+ export * from "./Accordion.js";
2
+ export * from "./Alert.js";
3
+ export * from "./AnimatedView.js";
4
+ export * from "./Avatar.js";
5
+ export * from "./Badge.js";
6
+ export * from "./BottomSheet.js";
7
+ export * from "./Button.js";
8
+ export * from "./Card.js";
9
+ export * from "./Carousel.js";
10
+ export * from "./Checkbox.js";
11
+ export * from "./Collapsible.js";
12
+ export * from "./Dialog.js";
13
+ export * from "./DismissKeyboard.js";
14
+ export * from "./keyboardDismiss.js";
15
+ export * from "./Drawer.js";
16
+ export * from "./DropdownMenu.js";
17
+ export * from "./EmptyState.js";
18
+ export * from "./ErrorBoundary.js";
19
+ export * from "./Hydrated.js";
20
+ export * from "./Icon.js";
21
+ export * from "./InputOTP.js";
22
+ export * from "./Item.js";
23
+ export * from "./KeyboardAvoidingView.js";
24
+ export * from "./Label.js";
25
+ export * from "./MaxWidthContainer.js";
26
+ export * from "./Notification.js";
27
+ export * from "./Screen.js";
28
+ export * from "./Popover.js";
29
+ export * from "./Progress.js";
30
+ export * from "./RadioGroup.js";
31
+ export * from "./SectionHeader.js";
32
+ export * from "./SegmentedControl.js";
33
+ export * from "./Select.js";
34
+ export * from "./Separator.js";
35
+ export * from "./Skeleton.js";
36
+ export * from "./Slider.js";
37
+ export * from "./StatCard.js";
38
+ export * from "./StatusBar.js";
39
+ export * from "./StyledText.js";
40
+ export * from "./Switch.js";
41
+ export * from "./Tabs.js";
42
+ export * from "./TextInput.js";
43
+ export * from "./Toggle.js";
44
+ export * from "./ToggleGroup.js";
45
+ export * from "./Tooltip.js";
46
+ export * from "./UIProvider.js";