@thewhileloop/whileui 4.1.0 → 4.1.2

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 (44) hide show
  1. package/README.md +120 -22
  2. package/dist/blocks/datepicker/date-range-picker-modal.js +3 -2
  3. package/dist/blocks/datepicker/datepicker-modal.js +3 -2
  4. package/dist/blocks/datepicker/time-picker-modal.js +3 -2
  5. package/dist/blocks/layout/confirm-action-sheet.js +3 -2
  6. package/dist/blocks/layout/index.d.ts +1 -0
  7. package/dist/blocks/layout/index.js +1 -0
  8. package/dist/blocks/layout/loading-state.d.ts +8 -0
  9. package/dist/blocks/layout/loading-state.js +12 -0
  10. package/dist/blocks/layout/sheet.js +21 -2
  11. package/dist/blocks/media/smart-image.js +5 -7
  12. package/dist/blocks/navigation/drawer-menu.js +10 -55
  13. package/dist/blocks/navigation/header.d.ts +2 -0
  14. package/dist/blocks/navigation/header.js +1 -1
  15. package/dist/components/alert/alert.js +4 -1
  16. package/dist/components/alert-dialog/alert-dialog.js +3 -2
  17. package/dist/components/avatar/avatar.d.ts +3 -2
  18. package/dist/components/avatar/avatar.js +19 -13
  19. package/dist/components/avatar/index.d.ts +1 -1
  20. package/dist/components/context-menu/context-menu.js +4 -3
  21. package/dist/components/dialog/dialog.js +4 -3
  22. package/dist/components/dropdown-menu/dropdown-menu.js +4 -3
  23. package/dist/components/hover-card/hover-card.js +5 -1
  24. package/dist/components/menubar/menubar.js +5 -4
  25. package/dist/components/popover/popover.js +5 -1
  26. package/dist/components/select/select.js +36 -15
  27. package/dist/components/text/text.d.ts +3 -0
  28. package/dist/components/text/text.js +2 -0
  29. package/dist/components/toast/toast.js +5 -1
  30. package/dist/components/toggle/toggle.d.ts +1 -1
  31. package/dist/components/toggle/toggle.js +1 -1
  32. package/dist/components/tooltip/tooltip.js +3 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/lib/overlay-host.d.ts +26 -0
  35. package/dist/lib/overlay-host.js +77 -0
  36. package/dist/lib/overlay-presence.d.ts +15 -0
  37. package/dist/lib/overlay-presence.js +55 -0
  38. package/dist/lib/recipes.d.ts +1 -1
  39. package/dist/lib/recipes.js +24 -5
  40. package/dist/lib/theme-bridge.d.ts +1 -1
  41. package/dist/lib/theme-bridge.js +52 -18
  42. package/dist/metro.cjs +33 -0
  43. package/dist/metro.d.cts +21 -0
  44. package/package.json +6 -2
@@ -14,7 +14,7 @@ export function Header({ title, subtitle, leftAction, rightActions, centerConten
14
14
  paddingVertical: visual.controlPaddingYDefault,
15
15
  },
16
16
  style,
17
- ], ...props, children: [_jsx(View, { className: "min-w-12", children: leftAction }), _jsx(View, { className: "flex-1 items-center", children: centerContent || (_jsxs(View, { className: "items-center", children: [title && (_jsx(Text, { className: "font-semibold text-foreground", style: typographyStyle(visual, 'body'), numberOfLines: 1, children: title })), subtitle && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'caption'), numberOfLines: 1, children: subtitle }))] })) }), _jsx(View, { className: "min-w-12 flex-row items-center justify-end gap-2", children: rightActions?.map((action) => (_jsx(Pressable, { accessibilityRole: "button", onPress: action.onPress, className: "rounded-full p-2 active:bg-accent", style: withInteractivePressableStyle(undefined, interaction, {
17
+ ], ...props, children: [_jsx(View, { className: "min-w-12", children: leftAction }), _jsx(View, { className: "flex-1 items-center", children: centerContent || (_jsxs(View, { className: "items-center", children: [title && (_jsx(Text, { className: "font-semibold text-foreground", style: typographyStyle(visual, 'body'), numberOfLines: 1, children: title })), subtitle && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'caption'), numberOfLines: 1, children: subtitle }))] })) }), _jsx(View, { className: "min-w-12 flex-row items-center justify-end gap-2", children: rightActions?.map((action) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: action.accessibilityLabel, onPress: action.onPress, className: "rounded-full p-2 active:bg-state-pressed", style: withInteractivePressableStyle(undefined, interaction, {
18
18
  pressedVariant: 'default',
19
19
  }), children: action.icon }, action.key))) })] }));
20
20
  }
@@ -22,7 +22,10 @@ const alertVariants = tv({
22
22
  });
23
23
  const Alert = React.forwardRef(({ className, variant, style, ...props }, ref) => {
24
24
  const visual = useVisualTokens();
25
- return (_jsx(View, { ref: ref, className: cn(alertVariants({ variant }), className), style: [
25
+ return (_jsx(View, { ref: ref,
26
+ // A status message that appears is announced without focus moving to it. Set
27
+ // before the spread so a decorative note can opt out.
28
+ accessibilityRole: "alert", accessibilityLiveRegion: "polite", className: cn(alertVariants({ variant }), className), style: [
26
29
  {
27
30
  borderRadius: surfaceRadius(visual, 'lg'),
28
31
  borderWidth: visual.borderWidthHairline,
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { createContext, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
- import { View, Modal, Pressable, Text, Alert, Platform, StyleSheet, } from 'react-native';
3
+ import { View, Pressable, Text, Alert, Platform, StyleSheet, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { composeEventHandlers } from '../../lib/compose-event-handlers';
6
6
  import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useFrostedSurface } from '../../lib/frosted-surface';
9
+ import { OverlayHost } from '../../lib/overlay-host';
9
10
  import { useVisualTokens } from '../../lib/visual-tokens';
10
11
  import { shadowStyle, typographyStyle } from '../../lib/recipes';
11
12
  function emptyNativeCopy() {
@@ -231,7 +232,7 @@ function AlertDialogContent({ className, children, presentation = 'modal', frost
231
232
  }
232
233
  return (_jsx(View, { pointerEvents: "box-none", style: [StyleSheet.absoluteFill, { zIndex: 100 }], children: themedBody }));
233
234
  }
234
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", presentationStyle: Platform.OS === 'ios' ? 'overFullScreen' : undefined, statusBarTranslucent: Platform.OS === 'android', navigationBarTranslucent: Platform.OS === 'android', onRequestClose: () => setOpen(false), children: themedBody }));
235
+ return (_jsx(OverlayHost, { open: open, animationType: "fade", onRequestClose: () => setOpen(false), children: themedBody }));
235
236
  }
236
237
  function useAlertDialogNativeCopy() {
237
238
  return useContext(AlertDialogContext).nativeCopyRef;
@@ -27,15 +27,16 @@ declare const avatarVariants: import("tailwind-variants").TVReturnType<{
27
27
  export interface AvatarProps extends ViewProps, VariantProps<typeof avatarVariants> {
28
28
  className?: string;
29
29
  }
30
+ export type AvatarImageStatus = 'loading' | 'loaded' | 'error';
30
31
  export interface AvatarImageProps extends Omit<ImageProps, 'style'> {
31
32
  className?: string;
32
- onLoadingStatusChange?: (status: 'loading' | 'loaded' | 'error') => void;
33
+ onLoadingStatusChange?: (status: AvatarImageStatus) => void;
33
34
  }
34
35
  export interface AvatarFallbackProps extends ViewProps {
35
36
  className?: string;
36
37
  }
37
38
  declare const Avatar: ForwardRefComponent<ViewInstance, AvatarProps>;
38
- declare function AvatarImage({ className, onLoadingStatusChange, ...props }: AvatarImageProps): React.JSX.Element;
39
+ declare function AvatarImage({ className, onLoadingStatusChange, onLoadStart, onLoad, onError, ...props }: AvatarImageProps): React.JSX.Element;
39
40
  declare namespace AvatarImage {
40
41
  var displayName: string;
41
42
  }
@@ -20,30 +20,36 @@ const avatarVariants = tv({
20
20
  },
21
21
  });
22
22
  const AvatarContext = React.createContext({
23
- imageLoaded: false,
24
- setImageLoaded: () => { },
23
+ imageStatus: 'loading',
24
+ setImageStatus: () => { },
25
25
  });
26
26
  const Avatar = React.forwardRef(({ className, size, ...props }, ref) => {
27
- const [imageLoaded, setImageLoaded] = useState(false);
28
- const contextValue = useMemo(() => ({ imageLoaded, setImageLoaded }), [imageLoaded]);
27
+ const [imageStatus, setImageStatus] = useState('loading');
28
+ const contextValue = useMemo(() => ({ imageStatus, setImageStatus }), [imageStatus]);
29
29
  return (_jsx(AvatarContext.Provider, { value: contextValue, children: _jsx(View, { ref: ref, className: cn(avatarVariants({ size }), className), ...props }) }));
30
30
  });
31
31
  Avatar.displayName = 'Avatar';
32
- function AvatarImage({ className, onLoadingStatusChange, ...props }) {
33
- const { setImageLoaded } = React.useContext(AvatarContext);
34
- return (_jsx(Image, { className: cn('aspect-square h-full w-full rounded-full', className), onLoad: () => {
35
- setImageLoaded(true);
32
+ function AvatarImage({ className, onLoadingStatusChange, onLoadStart, onLoad, onError, ...props }) {
33
+ const { setImageStatus } = React.useContext(AvatarContext);
34
+ return (_jsx(Image, { ...props, className: cn('aspect-square h-full w-full rounded-full', className), onLoadStart: () => {
35
+ setImageStatus('loading');
36
+ onLoadingStatusChange?.('loading');
37
+ onLoadStart?.();
38
+ }, onLoad: (event) => {
39
+ setImageStatus('loaded');
36
40
  onLoadingStatusChange?.('loaded');
37
- }, onError: () => {
38
- setImageLoaded(false);
41
+ onLoad?.(event);
42
+ }, onError: (event) => {
43
+ setImageStatus('error');
39
44
  onLoadingStatusChange?.('error');
40
- }, ...props }));
45
+ onError?.(event);
46
+ } }));
41
47
  }
42
48
  AvatarImage.displayName = 'AvatarImage';
43
49
  function AvatarFallback({ className, children, ...props }) {
44
- const { imageLoaded } = React.useContext(AvatarContext);
50
+ const { imageStatus } = React.useContext(AvatarContext);
45
51
  const visual = useVisualTokens();
46
- if (imageLoaded)
52
+ if (imageStatus === 'loaded')
47
53
  return null;
48
54
  return (_jsx(View, { className: cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className), ...props, children: typeof children === 'string' ? (_jsx(Text, { className: "font-medium text-muted-foreground", style: typographyStyle(visual, 'label'), children: children })) : (children) }));
49
55
  }
@@ -1 +1 @@
1
- export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarFallbackProps, } from './avatar';
1
+ export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarImageStatus, type AvatarFallbackProps, } from './avatar';
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { createContext, useContext, useMemo, useState } from 'react';
3
- import { View, Modal, Pressable, Text } from 'react-native';
3
+ import { View, Pressable, Text } from 'react-native';
4
+ import { OverlayHost } from '../../lib/overlay-host';
4
5
  import { cn } from '../../lib/cn';
5
6
  import { useFrostedSurface } from '../../lib/frosted-surface';
6
7
  import { useVisualTokens } from '../../lib/visual-tokens';
@@ -53,7 +54,7 @@ function ContextMenuContent({ className, children, frosted = false, blurIntensit
53
54
  frostedSurface.surfaceStyle,
54
55
  style,
55
56
  ];
56
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false, accessibilityRole: "menu", className: cn('w-full border-border p-2 pb-8 relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', className), style: contentStyle, onPress: (e) => e.stopPropagation(), ...props, children: [frostedSurface.overlay, children] }) }) }));
57
+ return (_jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false, accessibilityRole: "menu", className: cn('w-full border-border p-2 pb-8 relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', className), style: contentStyle, onPress: (e) => e.stopPropagation(), ...props, children: [frostedSurface.overlay, children] }) }) }));
57
58
  }
58
59
  function ContextMenuItem({ className, style, ...props }) {
59
60
  const { setOpen } = useContext(ContextMenuContext);
@@ -64,7 +65,7 @@ function ContextMenuItem({ className, style, ...props }) {
64
65
  paddingHorizontal: visual.controlPaddingXSm,
65
66
  paddingVertical: visual.controlPaddingYSm,
66
67
  };
67
- return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-accent', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
68
+ return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-state-pressed', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
68
69
  props.onPress?.(e);
69
70
  setOpen(false);
70
71
  }, ...props }));
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
3
- import { View, Modal, Pressable, Text, } from 'react-native';
3
+ import { View, Pressable, Text, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { composeEventHandlers } from '../../lib/compose-event-handlers';
6
6
  import { useThemeColors } from '../../lib/theme-colors';
7
7
  import { useFrostedSurface } from '../../lib/frosted-surface';
8
+ import { OverlayHost } from '../../lib/overlay-host';
8
9
  import { useVisualTokens } from '../../lib/visual-tokens';
9
10
  import { shadowStyle, typographyStyle } from '../../lib/recipes';
10
11
  const DialogContext = createContext({
@@ -47,7 +48,7 @@ function DialogContent({ className, children, frosted = false, blurIntensity, bl
47
48
  defaultBlurPreset: 'medium',
48
49
  });
49
50
  const contentStyle = [frostedSurface.surfaceStyle, style];
50
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: () => setOpen(false), children: _jsx(Pressable
51
+ return (_jsx(OverlayHost, { open: open, animationType: "fade", onRequestClose: () => setOpen(false), children: _jsx(Pressable
51
52
  // Tap-to-dismiss is a pointer affordance. Left accessible it would swallow the
52
53
  // whole panel into one element; screen reader users close from inside.
53
54
  , {
@@ -78,7 +79,7 @@ function DialogFooter({ className, ...props }) {
78
79
  }
79
80
  function DialogTitle({ className, style, ...props }) {
80
81
  const visual = useVisualTokens();
81
- return (_jsx(Text, { className: cn('font-semibold text-foreground', className), style: [typographyStyle(visual, 'title'), style], ...props }));
82
+ return (_jsx(Text, { accessibilityRole: "header", className: cn('font-semibold text-foreground', className), style: [typographyStyle(visual, 'title'), style], ...props }));
82
83
  }
83
84
  function DialogDescription({ className, style, ...props }) {
84
85
  const visual = useVisualTokens();
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { createContext, useContext, useMemo, useState } from 'react';
3
- import { View, Modal, Pressable, Text } from 'react-native';
3
+ import { View, Pressable, Text } from 'react-native';
4
+ import { OverlayHost } from '../../lib/overlay-host';
4
5
  import { cn } from '../../lib/cn';
5
6
  import { useFrostedSurface } from '../../lib/frosted-surface';
6
7
  import { useVisualTokens } from '../../lib/visual-tokens';
@@ -52,7 +53,7 @@ function DropdownMenuContent({ className, children, frosted = false, blurIntensi
52
53
  frostedSurface.surfaceStyle,
53
54
  style,
54
55
  ];
55
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false,
56
+ return (_jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false,
56
57
  // Names the container for the `menuitem` children below; without a `menu`
57
58
  // ancestor the role is invalid ARIA and assistive tech ignores it.
58
59
  accessibilityRole: "menu", className: cn('w-full border-border p-2 pb-8 relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', className), style: contentStyle, onPress: (e) => e.stopPropagation(), ...props, children: [frostedSurface.overlay, children] }) }) }));
@@ -66,7 +67,7 @@ function DropdownMenuItem({ className, style, ...props }) {
66
67
  paddingHorizontal: visual.controlPaddingXSm,
67
68
  paddingVertical: visual.controlPaddingYSm,
68
69
  };
69
- return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-accent', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
70
+ return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-state-pressed', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
70
71
  props.onPress?.(e);
71
72
  setOpen(false);
72
73
  }, ...props }));
@@ -4,6 +4,7 @@ import { Platform, StyleSheet } from 'react-native';
4
4
  import * as HoverCardPrimitive from '@rn-primitives/hover-card';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { useFrostedSurface } from '../../lib/frosted-surface';
7
+ import { useNestedOverlay, useOverlayPortalTarget } from '../../lib/overlay-host';
7
8
  import { useVisualTokens } from '../../lib/visual-tokens';
8
9
  import { useThemeColors } from '../../lib/theme-colors';
9
10
  import { shadowStyle, surfacePadding, surfaceRadius } from '../../lib/recipes';
@@ -22,6 +23,9 @@ const HoverCardContent = React.forwardRef(({ className, align = 'center', sideOf
22
23
  });
23
24
  const visual = useVisualTokens();
24
25
  const colors = useThemeColors();
26
+ const { open, onOpenChange } = HoverCardPrimitive.useRootContext();
27
+ const portalTarget = useOverlayPortalTarget(portalHost);
28
+ useNestedOverlay(open, () => onOpenChange(false));
25
29
  const resolvedStyle = StyleSheet.flatten([
26
30
  Platform.OS === 'web' ? undefined : contentStyles,
27
31
  {
@@ -33,7 +37,7 @@ const HoverCardContent = React.forwardRef(({ className, align = 'center', sideOf
33
37
  frostedSurface.surfaceStyle,
34
38
  style,
35
39
  ]);
36
- return (_jsx(HoverCardPrimitive.Portal, { hostName: portalHost, children: _jsx(HoverCardPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsxs(HoverCardPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, className: cn('z-50 w-64 border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: [frostedSurface.overlay, children] }) }) }));
40
+ return (_jsx(HoverCardPrimitive.Portal, { ...portalTarget, children: _jsx(HoverCardPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsxs(HoverCardPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, className: cn('z-50 w-64 border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: [frostedSurface.overlay, children] }) }) }));
37
41
  });
38
42
  HoverCardContent.displayName = 'HoverCardContent';
39
43
  const contentStyles = {
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo, useState } from 'react';
3
- import { View, Modal, Pressable, Text } from 'react-native';
3
+ import { View, Pressable, Text } from 'react-native';
4
+ import { OverlayHost } from '../../lib/overlay-host';
4
5
  import { cn } from '../../lib/cn';
5
6
  import { useFrostedSurface } from '../../lib/frosted-surface';
6
7
  import { useVisualTokens } from '../../lib/visual-tokens';
@@ -23,7 +24,7 @@ function MenubarTrigger({ className, style, ...props }) {
23
24
  const { setOpen } = useContext(MenubarMenuContext);
24
25
  const visual = useVisualTokens();
25
26
  const tokenStyle = controlRecipe(visual, 'sm');
26
- return (_jsx(Pressable, { accessibilityRole: "button", className: cn('active:bg-accent', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: () => setOpen(true), ...props }));
27
+ return (_jsx(Pressable, { accessibilityRole: "button", className: cn('active:bg-state-pressed', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: () => setOpen(true), ...props }));
27
28
  }
28
29
  function MenubarContent({ className, children, frosted = false, blurIntensity, blurTintToken, style, ...props }) {
29
30
  const { open, setOpen } = useContext(MenubarMenuContext);
@@ -48,7 +49,7 @@ function MenubarContent({ className, children, frosted = false, blurIntensity, b
48
49
  frostedSurface.surfaceStyle,
49
50
  style,
50
51
  ];
51
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false, accessibilityRole: "menu", className: cn('w-full border-border p-2 pb-8 relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', className), style: contentStyle, onPress: (e) => e.stopPropagation(), ...props, children: [frostedSurface.overlay, children] }) }) }));
52
+ return (_jsx(OverlayHost, { open: open, animationType: "slide", onRequestClose: () => setOpen(false), children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", onPress: () => setOpen(false), children: _jsxs(Pressable, { accessible: false, accessibilityRole: "menu", className: cn('w-full border-border p-2 pb-8 relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', className), style: contentStyle, onPress: (e) => e.stopPropagation(), ...props, children: [frostedSurface.overlay, children] }) }) }));
52
53
  }
53
54
  function MenubarItem({ className, style, ...props }) {
54
55
  const { setOpen } = useContext(MenubarMenuContext);
@@ -59,7 +60,7 @@ function MenubarItem({ className, style, ...props }) {
59
60
  paddingHorizontal: visual.controlPaddingXSm,
60
61
  paddingVertical: visual.controlPaddingYSm,
61
62
  };
62
- return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-accent', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
63
+ return (_jsx(Pressable, { accessibilityRole: "menuitem", className: cn('flex flex-row items-center active:bg-state-pressed', className), style: (state) => [tokenStyle, typeof style === 'function' ? style(state) : style], onPress: (e) => {
63
64
  props.onPress?.(e);
64
65
  setOpen(false);
65
66
  }, ...props }));
@@ -4,6 +4,7 @@ import { Platform, StyleSheet } from 'react-native';
4
4
  import * as PopoverPrimitive from '@rn-primitives/popover';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { useFrostedSurface } from '../../lib/frosted-surface';
7
+ import { useNestedOverlay, useOverlayPortalTarget } from '../../lib/overlay-host';
7
8
  import { useOverlayInsets } from '../../lib/overlay-insets';
8
9
  import { useVisualTokens } from '../../lib/visual-tokens';
9
10
  import { useThemeColors } from '../../lib/theme-colors';
@@ -23,6 +24,9 @@ const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffs
23
24
  });
24
25
  const visual = useVisualTokens();
25
26
  const colors = useThemeColors();
27
+ const { open, onOpenChange } = PopoverPrimitive.useRootContext();
28
+ const portalTarget = useOverlayPortalTarget(portalHost);
29
+ useNestedOverlay(open, () => onOpenChange(false));
26
30
  // Keeps the panel off the screen edge and out from under the notch.
27
31
  const defaultInsets = useOverlayInsets();
28
32
  const resolvedInsets = { ...defaultInsets, ...insets };
@@ -41,7 +45,7 @@ const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffs
41
45
  frostedSurface.surfaceStyle,
42
46
  style,
43
47
  ]);
44
- return (_jsx(PopoverPrimitive.Portal, { hostName: portalHost, children: _jsx(PopoverPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsxs(PopoverPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, insets: resolvedInsets, className: cn('z-50 w-72 border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: [frostedSurface.overlay, children] }) }) }));
48
+ return (_jsx(PopoverPrimitive.Portal, { ...portalTarget, children: _jsx(PopoverPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsxs(PopoverPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, insets: resolvedInsets, className: cn('z-50 w-72 border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: [frostedSurface.overlay, children] }) }) }));
45
49
  });
46
50
  PopoverContent.displayName = 'PopoverContent';
47
51
  const PopoverClose = React.forwardRef(({ className, ...props }, ref) => (_jsx(PopoverPrimitive.Close, { ref: ref, className: cn(className), ...props })));
@@ -8,6 +8,8 @@ import { cn } from '../../lib/cn';
8
8
  import { focusRing } from '../../lib/focus-ring';
9
9
  import { useInteractionTokens } from '../../lib/interaction-tokens';
10
10
  import { useFrostedSurface } from '../../lib/frosted-surface';
11
+ import { useNestedOverlay, useOverlayPortalTarget } from '../../lib/overlay-host';
12
+ import { useOverlayInsets } from '../../lib/overlay-insets';
11
13
  import { useVisualTokens } from '../../lib/visual-tokens';
12
14
  import { useThemeColors } from '../../lib/theme-colors';
13
15
  import { fieldRecipe, shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
@@ -59,21 +61,40 @@ const SelectContent = React.forwardRef(({ className, children, portalHost, posit
59
61
  });
60
62
  const visual = useVisualTokens();
61
63
  const colors = useThemeColors();
62
- return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsxs(SelectPrimitive.Content, { ref: ref, position: position, insets: insets, style: StyleSheet.flatten([
63
- {
64
- borderRadius: surfaceRadius(visual, 'lg'),
65
- borderWidth: visual.borderWidthHairline,
66
- },
67
- shadowStyle(visual, colors, 'md'),
68
- frostedSurface.surfaceStyle,
69
- styleProp,
70
- ]), className: cn('border-border relative z-50 min-w-32 overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', Platform.select({
71
- web: cn('max-h-96 overflow-y-auto overflow-x-hidden', props.side === 'bottom' && 'translate-y-1', props.side === 'top' && '-translate-y-1'),
64
+ const { open, onOpenChange } = SelectPrimitive.useRootContext();
65
+ const portalTarget = useOverlayPortalTarget(portalHost);
66
+ useNestedOverlay(open, () => onOpenChange(false));
67
+ // Keeps the listbox off the screen edge and clear of the notch and home indicator
68
+ // without every caller reading the safe area itself.
69
+ const defaultInsets = useOverlayInsets();
70
+ const resolvedInsets = { ...defaultInsets, ...insets };
71
+ // Radix applies its collision variables to the listbox wrapper, while
72
+ // @rn-primitives renders our visual surface one level inside it. Keep the sizing
73
+ // on that surface as inline web CSS: the Viewport's own `width: 100%` otherwise
74
+ // wins class merging and lets the longest option size the outer popper.
75
+ const webSizingStyle = Platform.OS === 'web' && position === 'popper'
76
+ ? {
77
+ boxSizing: 'border-box',
78
+ width: 'var(--radix-select-trigger-width)',
79
+ maxWidth: 'var(--radix-select-content-available-width)',
80
+ maxHeight: 'var(--radix-select-content-available-height)',
81
+ }
82
+ : undefined;
83
+ return (_jsx(SelectPrimitive.Portal, { ...portalTarget, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsxs(SelectPrimitive.Content, { ref: ref, position: position, insets: resolvedInsets, style: {
84
+ ...webSizingStyle,
85
+ ...StyleSheet.flatten([
86
+ {
87
+ borderRadius: surfaceRadius(visual, 'lg'),
88
+ borderWidth: visual.borderWidthHairline,
89
+ },
90
+ shadowStyle(visual, colors, 'md'),
91
+ frostedSurface.surfaceStyle,
92
+ styleProp,
93
+ ]),
94
+ }, className: cn('border-border relative z-50 overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', Platform.select({
95
+ web: cn('overflow-y-auto overflow-x-hidden', props.side === 'bottom' && 'translate-y-1', props.side === 'top' && '-translate-y-1'),
72
96
  native: 'p-1',
73
- }), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: cn('p-1', position === 'popper' &&
74
- Platform.select({
75
- web: 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
76
- })), children: children })] }) }) }) }));
97
+ }), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: "p-1", children: children })] }) }) }) }));
77
98
  });
78
99
  SelectContent.displayName = 'SelectContent';
79
100
  const SelectItem = React.forwardRef(({ className, children, style: styleProp, ...props }, ref) => {
@@ -90,7 +111,7 @@ const SelectItem = React.forwardRef(({ className, children, style: styleProp, ..
90
111
  },
91
112
  styleProp,
92
113
  props.disabled ? { opacity: interaction.disabledOpacity } : null,
93
- ]), ...props, children: [_jsx(View, { className: "absolute right-3 flex h-4 w-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(View, { className: "border-primary h-2.5 w-1.5 rotate-45 border-b-2 border-r-2 -mt-0.5" }) }) }), children, _jsx(SelectPrimitive.ItemText, { className: "text-foreground", style: typographyStyle(visual, 'body') })] }));
114
+ ]), ...props, children: [_jsx(View, { className: "absolute right-3 flex h-4 w-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(View, { className: "border-primary h-2.5 w-1.5 rotate-45 border-b-2 border-r-2 -mt-0.5" }) }) }), children, _jsx(SelectPrimitive.ItemText, { className: "min-w-0 flex-1 text-foreground web:whitespace-normal web:break-words", style: typographyStyle(visual, 'body') })] }));
94
115
  });
95
116
  SelectItem.displayName = 'SelectItem';
96
117
  function SelectItemIcon({ className, children }) {
@@ -10,6 +10,7 @@ declare const textVariants: import("tailwind-variants").TVReturnType<{
10
10
  body: "font-normal";
11
11
  caption: "text-muted-foreground font-normal";
12
12
  label: "font-medium leading-tight";
13
+ eyebrow: "text-muted-foreground font-medium";
13
14
  code: "font-mono bg-muted rounded px-1 py-0.5";
14
15
  };
15
16
  size: {
@@ -30,6 +31,7 @@ declare const textVariants: import("tailwind-variants").TVReturnType<{
30
31
  body: "font-normal";
31
32
  caption: "text-muted-foreground font-normal";
32
33
  label: "font-medium leading-tight";
34
+ eyebrow: "text-muted-foreground font-medium";
33
35
  code: "font-mono bg-muted rounded px-1 py-0.5";
34
36
  };
35
37
  size: {
@@ -50,6 +52,7 @@ declare const textVariants: import("tailwind-variants").TVReturnType<{
50
52
  body: "font-normal";
51
53
  caption: "text-muted-foreground font-normal";
52
54
  label: "font-medium leading-tight";
55
+ eyebrow: "text-muted-foreground font-medium";
53
56
  code: "font-mono bg-muted rounded px-1 py-0.5";
54
57
  };
55
58
  size: {
@@ -16,6 +16,7 @@ const textVariants = tv({
16
16
  body: 'font-normal',
17
17
  caption: 'text-muted-foreground font-normal',
18
18
  label: 'font-medium leading-tight',
19
+ eyebrow: 'text-muted-foreground font-medium',
19
20
  code: 'font-mono bg-muted rounded px-1 py-0.5',
20
21
  },
21
22
  size: {
@@ -40,6 +41,7 @@ const VARIANT_ROLES = {
40
41
  body: 'body',
41
42
  caption: 'caption',
42
43
  label: 'label',
44
+ eyebrow: 'label',
43
45
  code: 'label',
44
46
  };
45
47
  const SIZE_ROLES = {
@@ -86,10 +86,14 @@ function Toast({ toast: toastData, onDismiss, className, style, ...props }) {
86
86
  paddingVertical: visual.controlPaddingYSm,
87
87
  }, children: _jsx(Text, { className: cn('font-medium', textColor), style: typographyStyle(visual, 'label'), children: action.label }) })), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Dismiss", onPress: onDismiss, className: "p-1", children: _jsx(Text, { className: cn(textColor), style: typographyStyle(visual, 'emphasis'), children: "\u00D7" }) })] })] }));
88
88
  }
89
+ function ToastContainerItem({ toast, dismiss, }) {
90
+ const handleDismiss = useCallback(() => dismiss(toast.id), [dismiss, toast.id]);
91
+ return (_jsx(View, { className: "mb-2", children: _jsx(Toast, { toast: toast, onDismiss: handleDismiss }) }));
92
+ }
89
93
  export function ToastContainer({ position = 'top', className, style, ...props }) {
90
94
  const { toasts, dismiss } = useToast();
91
95
  const visual = useVisualTokens();
92
96
  const positionClass = position === 'top' ? 'top-0' : 'bottom-0';
93
- return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: toasts.map((toast) => (_jsx(View, { className: "mb-2", children: _jsx(Toast, { toast: toast, onDismiss: () => dismiss(toast.id) }) }, toast.id))) }));
97
+ return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: toasts.map((toast) => (_jsx(ToastContainerItem, { toast: toast, dismiss: dismiss }, toast.id))) }));
94
98
  }
95
99
  export { Toast, toastVariants };
@@ -15,7 +15,7 @@ declare const toggleVariants: import("tailwind-variants").TVReturnType<{
15
15
  true: "bg-accent";
16
16
  false: "";
17
17
  };
18
- }, undefined, "inline-flex items-center justify-center active:bg-accent", {
18
+ }, undefined, "inline-flex items-center justify-center active:bg-state-pressed", {
19
19
  variant: {
20
20
  default: "bg-transparent";
21
21
  outline: "border border-input bg-transparent";
@@ -12,7 +12,7 @@ const ToggleContext = createContext({
12
12
  size: 'default',
13
13
  });
14
14
  const toggleVariants = tv({
15
- base: 'inline-flex items-center justify-center active:bg-accent',
15
+ base: 'inline-flex items-center justify-center active:bg-state-pressed',
16
16
  variants: {
17
17
  variant: {
18
18
  default: 'bg-transparent',
@@ -3,6 +3,7 @@ import * as React from 'react';
3
3
  import { Platform, StyleSheet, Text } from 'react-native';
4
4
  import * as TooltipPrimitive from '@rn-primitives/tooltip';
5
5
  import { cn } from '../../lib/cn';
6
+ import { useOverlayPortalTarget } from '../../lib/overlay-host';
6
7
  import { useVisualTokens } from '../../lib/visual-tokens';
7
8
  import { useThemeColors } from '../../lib/theme-colors';
8
9
  import { shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
@@ -14,6 +15,7 @@ TooltipTrigger.displayName = 'TooltipTrigger';
14
15
  const TooltipContent = React.forwardRef(({ className, sideOffset = 4, portalHost, children, style, ...props }, ref) => {
15
16
  const visual = useVisualTokens();
16
17
  const colors = useThemeColors();
18
+ const portalTarget = useOverlayPortalTarget(portalHost);
17
19
  const resolvedStyle = StyleSheet.flatten([
18
20
  Platform.OS === 'web' ? undefined : contentStyles,
19
21
  {
@@ -26,7 +28,7 @@ const TooltipContent = React.forwardRef(({ className, sideOffset = 4, portalHost
26
28
  shadowStyle(visual, colors, 'md'),
27
29
  style,
28
30
  ]);
29
- return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(TooltipPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn('z-50 overflow-hidden bg-primary', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: typeof children === 'string' ? (_jsx(Text, { className: "text-primary-foreground", style: typographyStyle(visual, 'caption'), children: children })) : (children) }) }) }));
31
+ return (_jsx(TooltipPrimitive.Portal, { ...portalTarget, children: _jsx(TooltipPrimitive.Overlay, { style: Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined, children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn('z-50 overflow-hidden bg-primary', 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), style: resolvedStyle, ...props, children: typeof children === 'string' ? (_jsx(Text, { className: "text-primary-foreground", style: typographyStyle(visual, 'caption'), children: children })) : (children) }) }) }));
30
32
  });
31
33
  TooltipContent.displayName = 'TooltipContent';
32
34
  const contentStyles = {
package/dist/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export { Spinner, spinnerVariants, type SpinnerProps } from './components/spinne
40
40
  export { Textarea, textareaVariants, type TextareaProps } from './components/textarea';
41
41
  export { Skeleton, skeletonVariants, type SkeletonProps } from './components/skeleton';
42
42
  export { AspectRatio, type AspectRatioProps } from './components/aspect-ratio';
43
- export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarFallbackProps, } from './components/avatar';
43
+ export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarImageStatus, type AvatarFallbackProps, } from './components/avatar';
44
44
  export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, type AccordionProps, type AccordionItemProps, type AccordionTriggerProps, type AccordionContentProps, } from './components/accordion';
45
45
  export { Collapsible, CollapsibleTrigger, CollapsibleContent, type CollapsibleProps, type CollapsibleTriggerProps, type CollapsibleContentProps, } from './components/collapsible';
46
46
  export { Container, containerVariants, type ContainerProps } from './components/container';
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { type ModalProps } from 'react-native';
3
+ import type * as PopoverPrimitive from '@rn-primitives/popover';
4
+ type PortalContainer = React.ComponentProps<typeof PopoverPrimitive.Portal>['container'];
5
+ export interface OverlayHostProps extends Pick<ModalProps, 'animationType' | 'onShow' | 'onDismiss'> {
6
+ open: boolean;
7
+ /** Android back and web Escape. Omit for a host that only closes from inside. */
8
+ onRequestClose?: () => void;
9
+ children: React.ReactNode;
10
+ }
11
+ /**
12
+ * The platform `Modal` every WhileUI overlay is hosted in. Each Modal is its own window,
13
+ * so the platform already routes Android back and web Escape to the topmost one and, on
14
+ * web, traps and restores focus. The host adds what a root-level portal cannot reach:
15
+ * nested Select, Popover, Tooltip and HoverCard content renders inside this window, and a
16
+ * dismiss request closes the topmost nested overlay before the host itself.
17
+ */
18
+ export declare function OverlayHost({ open, onRequestClose, children, animationType, onShow, onDismiss, }: OverlayHostProps): React.JSX.Element;
19
+ /** Portal props for content rendered inside the nearest host. An explicit `portalHost` wins. */
20
+ export declare function useOverlayPortalTarget(portalHost?: string): {
21
+ hostName?: string;
22
+ container?: PortalContainer;
23
+ };
24
+ /** Lets the nearest host close this overlay before itself while `open` is true. */
25
+ export declare function useNestedOverlay(open: boolean, close: () => void): void;
26
+ export {};
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
+ import { Modal, Platform, View } from 'react-native';
4
+ import { PortalHost } from '@rn-primitives/portal';
5
+ const OverlayHostContext = createContext(null);
6
+ // Radix closes a nested overlay on the Escape keydown; React Native Web closes the Modal
7
+ // on the keyup of the same press. Seeing the keydown first lets the host drop that keyup.
8
+ const escapeKeyTarget = Platform.OS === 'web' ? globalThis.document : undefined;
9
+ /**
10
+ * The platform `Modal` every WhileUI overlay is hosted in. Each Modal is its own window,
11
+ * so the platform already routes Android back and web Escape to the topmost one and, on
12
+ * web, traps and restores focus. The host adds what a root-level portal cannot reach:
13
+ * nested Select, Popover, Tooltip and HoverCard content renders inside this window, and a
14
+ * dismiss request closes the topmost nested overlay before the host itself.
15
+ */
16
+ export function OverlayHost({ open, onRequestClose, children, animationType, onShow, onDismiss, }) {
17
+ const hostName = useId();
18
+ const nested = useRef([]);
19
+ const consumedByNested = useRef(false);
20
+ const [container, setContainer] = useState(undefined);
21
+ const register = useCallback((overlay) => {
22
+ nested.current = [...nested.current, overlay];
23
+ return () => {
24
+ nested.current = nested.current.filter((entry) => entry !== overlay);
25
+ };
26
+ }, []);
27
+ const requestClose = useCallback(() => {
28
+ if (consumedByNested.current) {
29
+ consumedByNested.current = false;
30
+ return;
31
+ }
32
+ const topmost = nested.current.at(-1);
33
+ if (topmost) {
34
+ topmost.close();
35
+ return;
36
+ }
37
+ onRequestClose?.();
38
+ }, [onRequestClose]);
39
+ useEffect(() => {
40
+ if (!escapeKeyTarget || !open)
41
+ return;
42
+ const markConsumed = (event) => {
43
+ if (event.key === 'Escape' && nested.current.length > 0)
44
+ consumedByNested.current = true;
45
+ };
46
+ escapeKeyTarget.addEventListener('keydown', markConsumed, true);
47
+ return () => {
48
+ escapeKeyTarget.removeEventListener('keydown', markConsumed, true);
49
+ consumedByNested.current = false;
50
+ };
51
+ }, [open]);
52
+ const attachContainer = useCallback((node) => {
53
+ setContainer(node);
54
+ }, []);
55
+ const value = useMemo(() => ({ hostName, container, register }), [hostName, container, register]);
56
+ return (_jsx(Modal, { visible: open, transparent: true, animationType: animationType, presentationStyle: "overFullScreen", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: requestClose, onShow: onShow, onDismiss: onDismiss, children: _jsxs(OverlayHostContext.Provider, { value: value, children: [children, Platform.OS === 'web' ? _jsx(View, { ref: attachContainer }) : _jsx(PortalHost, { name: hostName })] }) }));
57
+ }
58
+ /** Portal props for content rendered inside the nearest host. An explicit `portalHost` wins. */
59
+ export function useOverlayPortalTarget(portalHost) {
60
+ const host = useContext(OverlayHostContext);
61
+ if (portalHost !== undefined)
62
+ return { hostName: portalHost };
63
+ return { hostName: host?.hostName, container: host?.container };
64
+ }
65
+ /** Lets the nearest host close this overlay before itself while `open` is true. */
66
+ export function useNestedOverlay(open, close) {
67
+ const host = useContext(OverlayHostContext);
68
+ const closeRef = useRef(close);
69
+ useEffect(() => {
70
+ closeRef.current = close;
71
+ });
72
+ useEffect(() => {
73
+ if (!host || !open)
74
+ return;
75
+ return host.register({ close: () => closeRef.current() });
76
+ }, [host, open]);
77
+ }