@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.
Files changed (140) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/LLM_USAGE.md +233 -35
  3. package/README.md +504 -103
  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 +29 -1
  11. package/dist/components/BottomSheet.js +107 -30
  12. package/dist/components/Button.d.ts +20 -4
  13. package/dist/components/Button.js +60 -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 +87 -16
  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 +5 -2
  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 +73 -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 +20 -30
  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 +71 -26
  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/index.d.ts +46 -44
  71. package/dist/components/index.js +2 -0
  72. package/dist/components/keyboardDismiss.js +12 -4
  73. package/dist/components/nativeTextField.android.js +13 -9
  74. package/dist/constants/colors.d.ts +40 -5
  75. package/dist/constants/colors.js +26 -8
  76. package/dist/constants/fonts.d.ts +41 -2
  77. package/dist/constants/fonts.js +46 -7
  78. package/dist/constants/index.d.ts +5 -4
  79. package/dist/constants/index.js +1 -0
  80. package/dist/constants/interaction.d.ts +18 -0
  81. package/dist/constants/interaction.js +18 -0
  82. package/dist/constants/spacing.d.ts +2 -1
  83. package/dist/constants/spacing.js +2 -1
  84. package/dist/design-system.json +154 -2
  85. package/dist/hooks/index.d.ts +12 -7
  86. package/dist/hooks/index.js +5 -0
  87. package/dist/hooks/useDimensions.d.ts +13 -8
  88. package/dist/hooks/useDimensions.js +123 -48
  89. package/dist/hooks/useFocusVisible.d.ts +30 -0
  90. package/dist/hooks/useFocusVisible.js +45 -0
  91. package/dist/hooks/useFontStyle.d.ts +2 -2
  92. package/dist/hooks/useFontStyle.js +3 -2
  93. package/dist/hooks/useHydrated.d.ts +13 -0
  94. package/dist/hooks/useHydrated.js +19 -0
  95. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  96. package/dist/hooks/useNotificationOffset.js +34 -0
  97. package/dist/hooks/useResources.d.ts +21 -20
  98. package/dist/hooks/useResources.js +72 -68
  99. package/dist/hooks/useScalePress.d.ts +8 -5
  100. package/dist/hooks/useScalePress.js +16 -9
  101. package/dist/hooks/useShape.d.ts +24 -0
  102. package/dist/hooks/useShape.js +24 -0
  103. package/dist/hooks/useStaggeredEntrance.js +9 -7
  104. package/dist/hooks/useTheme.d.ts +3 -3
  105. package/dist/hooks/useTheme.js +177 -66
  106. package/dist/hooks/useWindowInsets.d.ts +13 -0
  107. package/dist/hooks/useWindowInsets.js +23 -0
  108. package/dist/index.d.ts +5 -5
  109. package/dist/lib/composeRefs.d.ts +8 -0
  110. package/dist/lib/composeRefs.js +20 -0
  111. package/dist/lib/haptics.d.ts +12 -0
  112. package/dist/lib/haptics.js +21 -0
  113. package/dist/lib/index.d.ts +6 -5
  114. package/dist/lib/index.js +1 -0
  115. package/dist/lib/interFonts.d.ts +15 -0
  116. package/dist/lib/interFonts.js +1 -0
  117. package/dist/lib/interFonts.native.d.ts +3 -0
  118. package/dist/lib/interFonts.native.js +20 -0
  119. package/dist/lib/popoverPlacement.d.ts +36 -0
  120. package/dist/lib/popoverPlacement.js +29 -0
  121. package/dist/lib/themedStyles.d.ts +1 -1
  122. package/dist/lib/useAnimatedValue.d.ts +15 -0
  123. package/dist/lib/useAnimatedValue.js +19 -0
  124. package/dist/lib/webHitTarget.d.ts +40 -0
  125. package/dist/lib/webHitTarget.js +37 -0
  126. package/dist/state/feedbackStore.d.ts +24 -0
  127. package/dist/state/feedbackStore.js +16 -0
  128. package/dist/state/globalUIStore.d.ts +27 -0
  129. package/dist/state/globalUIStore.js +23 -0
  130. package/dist/state/index.d.ts +7 -5
  131. package/dist/state/index.js +2 -0
  132. package/dist/state/initialScheme.d.ts +27 -0
  133. package/dist/state/initialScheme.js +29 -0
  134. package/dist/state/notify.d.ts +1 -1
  135. package/dist/state/themeColorScope.d.ts +1 -1
  136. package/dist/state/themeStore.d.ts +69 -16
  137. package/dist/state/themeStore.js +65 -38
  138. package/llms-full.md +56 -8
  139. package/llms.txt +10 -1
  140. package/package.json +69 -2
@@ -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
+ }
@@ -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";
@@ -16,6 +16,7 @@ export * from "./Drawer.js";
16
16
  export * from "./DropdownMenu.js";
17
17
  export * from "./EmptyState.js";
18
18
  export * from "./ErrorBoundary.js";
19
+ export * from "./Hydrated.js";
19
20
  export * from "./Icon.js";
20
21
  export * from "./InputOTP.js";
21
22
  export * from "./Item.js";
@@ -23,6 +24,7 @@ export * from "./KeyboardAvoidingView.js";
23
24
  export * from "./Label.js";
24
25
  export * from "./MaxWidthContainer.js";
25
26
  export * from "./Notification.js";
27
+ export * from "./Screen.js";
26
28
  export * from "./Popover.js";
27
29
  export * from "./Progress.js";
28
30
  export * from "./RadioGroup.js";
@@ -28,7 +28,7 @@
28
28
  * slop; release coordinates also enforce it when move delivery was missed.
29
29
  * Cancellation and multitouch drop the tap without rearming another finger.
30
30
  */
31
- import { useMemo, useRef } from "react";
31
+ import { useLayoutEffect, useMemo, useRef } from "react";
32
32
  import { Platform } from "react-native";
33
33
  import { KeyboardController, useKeyboardState } from "./keyboardController";
34
34
  import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
@@ -164,8 +164,12 @@ function createTapTracker() {
164
164
  * for that.
165
165
  */
166
166
  export function useTextInputSurfaceResponder(focus) {
167
+ // Latest `focus` for the memoized handlers, synced after each commit (before
168
+ // any touch can be dispatched) instead of written during render.
167
169
  const focusRef = useRef(focus);
168
- focusRef.current = focus;
170
+ useLayoutEffect(() => {
171
+ focusRef.current = focus;
172
+ });
169
173
  return useMemo(() => {
170
174
  const tap = createTapTracker();
171
175
  return {
@@ -219,7 +223,9 @@ export function resetAncestorClaimWarningForTests() {
219
223
  */
220
224
  export function useAncestorClaimWarning(inner, message) {
221
225
  const messageRef = useRef(message);
222
- messageRef.current = message;
226
+ useLayoutEffect(() => {
227
+ messageRef.current = message;
228
+ });
223
229
  return useMemo(() => {
224
230
  if (!__DEV__ || Platform.OS !== "android")
225
231
  return inner;
@@ -249,7 +255,9 @@ export function useAncestorClaimWarning(inner, message) {
249
255
  export function useKeyboardDismissResponder() {
250
256
  const isVisible = useKeyboardState((state) => state.isVisible);
251
257
  const isVisibleRef = useRef(isVisible);
252
- isVisibleRef.current = isVisible;
258
+ useLayoutEffect(() => {
259
+ isVisibleRef.current = isVisible;
260
+ });
253
261
  return useMemo(() => {
254
262
  if (Platform.OS === "web")
255
263
  return {};
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
34
  * Only `@expo/ui/jetpack-compose` and `@expo/ui/jetpack-compose/modifiers` are
35
35
  * imported, both in the peer's export map since SDK 56.
36
36
  */
37
- import { useImperativeHandle, useRef } from "react";
37
+ import { useImperativeHandle, useRef, useState } from "react";
38
38
  import { BasicTextField, Box, Text, useNativeState, } from "@expo/ui/jetpack-compose";
39
39
  import { fillMaxWidth, onSizeChanged, padding, paddingAll, semantics, testID as testIDModifier, } from "@expo/ui/jetpack-compose/modifiers";
40
40
  // region Prop mapping (ported verbatim from @expo/ui's TextInput/utils.ts)
@@ -145,8 +145,10 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
145
145
  const numberOfLines = numberOfLinesProp ?? rows;
146
146
  const keyboardType = keyboardTypeProp ?? inputModeToKeyboardType(inputMode);
147
147
  const returnKeyType = returnKeyTypeProp ?? enterKeyHintToReturnKeyType(enterKeyHint);
148
- const initialFallbackRef = useRef(defaultValue ?? "");
149
- const fallback = useNativeState(initialFallbackRef.current);
148
+ // Pinned to the first render's `defaultValue`, as an uncontrolled default
149
+ // is, whatever `useNativeState` does with a later argument.
150
+ const [initialFallback] = useState(defaultValue ?? "");
151
+ const fallback = useNativeState(initialFallback);
150
152
  const state = (value ?? fallback);
151
153
  const innerRef = useRef(null);
152
154
  const isFocusedRef = useRef(false);
@@ -176,12 +178,14 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
176
178
  // Sticky for the life of the mounted field: revealing a password
177
179
  // (`secureTextEntry` -> false on the same element) must keep the password
178
180
  // keyboard options unchanged so the IME neither restarts nor starts
179
- // suggesting/learning the now-visible text. Written during render like the
180
- // wrapper's own `activeSecureRef`; a fresh mount starts from the prop.
181
- const everSecureRef = useRef(!!secureTextEntry);
182
- if (secureTextEntry)
183
- everSecureRef.current = true;
184
- const passwordField = everSecureRef.current;
181
+ // suggesting/learning the now-visible text. The latch is state adjusted
182
+ // during render (React re-runs this render before committing it), and the
183
+ // `||` makes the very render that turns secure on already read as a password
184
+ // field; a fresh mount starts from the prop.
185
+ const [everSecure, setEverSecure] = useState(!!secureTextEntry);
186
+ if (secureTextEntry && !everSecure)
187
+ setEverSecure(true);
188
+ const passwordField = everSecure || !!secureTextEntry;
185
189
  const mappedKeyboardType = keyboardType ? mapKeyboardType(keyboardType) : undefined;
186
190
  const resolvedKeyboardType = passwordField
187
191
  ? passwordKeyboardType(mappedKeyboardType)
@@ -1,4 +1,4 @@
1
- import { navigationFonts } from "./fonts";
1
+ import { navigationFonts } from "./fonts.js";
2
2
  /**
3
3
  * Raw color palette - use semantic colors in components instead
4
4
  *
@@ -38,10 +38,36 @@ declare const palette: {
38
38
  readonly red500: "#EF4444";
39
39
  readonly red400: "#F87171";
40
40
  };
41
+ /**
42
+ * Extra color tokens an app declares for itself, so it stops keeping a side
43
+ * palette. Empty here; an app augments it once and the keys become part of
44
+ * `ThemeColors` everywhere — `setColors`, `ThemeColorScope`,
45
+ * `useTheme().theme.colors`, `getThemeCssVariables`:
46
+ *
47
+ * ```ts
48
+ * declare module "@mrmeg/expo-ui/constants" {
49
+ * interface ThemeColorExtensions {
50
+ * brandGold: string;
51
+ * }
52
+ * }
53
+ * useThemeStore.getState().setColors({
54
+ * light: { brandGold: "#c9a227" },
55
+ * dark: { brandGold: "#ffe066" },
56
+ * });
57
+ * ```
58
+ *
59
+ * The package ships no value for an extension key: provide it in both schemes
60
+ * or the token is `undefined` at runtime in the scheme that lacks it. On web
61
+ * the built-in tokens are `var(--c-*)` references and extension values are
62
+ * literals per scheme, so with any override `theme.colors` is a new object
63
+ * when the scheme changes (the identity caveat every override already has).
64
+ */
65
+ export interface ThemeColorExtensions {
66
+ }
41
67
  /**
42
68
  * Semantic color interface - what colors mean in the UI
43
69
  */
44
- export interface ThemeColors {
70
+ export interface ThemeColors extends ThemeColorExtensions {
45
71
  surfaceSunken: string;
46
72
  background: string;
47
73
  foreground: string;
@@ -86,14 +112,21 @@ export interface Colors {
86
112
  light: Theme;
87
113
  dark: Theme;
88
114
  }
115
+ /**
116
+ * The package's own tokens: `ThemeColors` minus whatever an app added through
117
+ * `ThemeColorExtensions`. The default palettes, the CSS-variable table and
118
+ * `resolveRawColor` are typed on this, because the package has no value for an
119
+ * extension key.
120
+ */
121
+ export type ThemeColorTokens = Omit<ThemeColors, keyof ThemeColorExtensions>;
89
122
  /**
90
123
  * Raw hex/rgba theme colors, always literal strings on every platform. Use
91
124
  * these for sinks that cannot take CSS `var()` — e.g. `<meta name="theme-color">`,
92
125
  * color parsing/math — and for generating the CSS variable definitions below.
93
126
  */
94
127
  export declare const rawThemeColors: {
95
- light: ThemeColors;
96
- dark: ThemeColors;
128
+ light: ThemeColorTokens;
129
+ dark: ThemeColorTokens;
97
130
  };
98
131
  /**
99
132
  * Resolves a theme color value to a literal color string. On web, theme
@@ -114,7 +147,9 @@ export declare function resolveRawColor(color: string, scheme: "light" | "dark")
114
147
  * paint before any theme script stamps `data-theme`.
115
148
  *
116
149
  * Apps that override brand colors can pass those overrides so the emitted
117
- * CSS matches their theme.
150
+ * CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
151
+ * in the overrides get a variable too, per scheme that names them; the
152
+ * package has no default to fall back to for those.
118
153
  */
119
154
  export declare function getThemeCssVariables(overrides?: {
120
155
  light?: Partial<ThemeColors>;
@@ -139,6 +139,7 @@ const themeColorTokens = Object.keys(lightTheme.colors);
139
139
  function cssVarName(token) {
140
140
  return `--c-${token.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
141
141
  }
142
+ const themeColorTokenSet = new Set(themeColorTokens);
142
143
  // Reverse lookup: "var(--c-surface-sunken)" → "surfaceSunken"
143
144
  const varToToken = new Map(themeColorTokens.map((token) => [`var(${cssVarName(token)})`, token]));
144
145
  /**
@@ -176,17 +177,27 @@ function toRgbTriplet(value) {
176
177
  * paint before any theme script stamps `data-theme`.
177
178
  *
178
179
  * Apps that override brand colors can pass those overrides so the emitted
179
- * CSS matches their theme.
180
+ * CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
181
+ * in the overrides get a variable too, per scheme that names them; the
182
+ * package has no default to fall back to for those.
180
183
  */
181
184
  export function getThemeCssVariables(overrides) {
182
- const declarations = (scheme) => themeColorTokens
183
- .map((token) => {
184
- const value = overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token];
185
+ const extensionTokens = [
186
+ ...Object.keys(overrides?.light ?? {}),
187
+ ...Object.keys(overrides?.dark ?? {}),
188
+ ].filter((token, index, all) => !themeColorTokenSet.has(token) && all.indexOf(token) === index);
189
+ const declaration = (token, value) => {
185
190
  const name = cssVarName(token);
186
191
  const rgb = toRgbTriplet(value);
187
192
  return `${name}: ${value};${rgb ? ` ${name}-rgb: ${rgb};` : ""}`;
188
- })
189
- .join("\n ");
193
+ };
194
+ const declarations = (scheme) => [
195
+ ...themeColorTokens.map((token) => declaration(token, overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token])),
196
+ ...extensionTokens.flatMap((token) => {
197
+ const value = overrides?.[scheme]?.[token];
198
+ return typeof value === "string" ? [declaration(token, value)] : [];
199
+ }),
200
+ ].join("\n ");
190
201
  return `
191
202
  :root {
192
203
  ${declarations("light")}
@@ -209,10 +220,17 @@ export function getThemeCssVariables(overrides) {
209
220
  // values (no CSS engine). The `navigation` maps stay literal on both
210
221
  // platforms: @react-navigation internals color-parse their theme values.
211
222
  const webVarColors = Object.fromEntries(themeColorTokens.map((token) => [token, `var(${cssVarName(token)})`]));
223
+ // Web: both schemes hold the same `var(--c-*)` references, so they share ONE
224
+ // `colors` object. A scheme switch then keeps `theme.colors` identical, so
225
+ // anything memoized on it keeps its result; the CSS variables do the
226
+ // re-theming. `colors.light` and `colors.dark` stay distinct objects:
227
+ // `dark`, `navigation`, and `fonts` differ per scheme.
228
+ // The base themes carry the package tokens only; extension keys (typed on
229
+ // `ThemeColors` once an app augments it) arrive through `setColors`.
212
230
  export const colors = Platform.OS === "web"
213
231
  ? {
214
- light: { ...lightTheme, colors: { ...webVarColors } },
215
- dark: { ...darkTheme, colors: { ...webVarColors } },
232
+ light: { ...lightTheme, colors: webVarColors },
233
+ dark: { ...darkTheme, colors: webVarColors },
216
234
  }
217
235
  : {
218
236
  light: lightTheme,