orn-ui 0.2.2 → 0.2.4

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 (90) hide show
  1. package/lib/commonjs/molecules/Steps.js +14 -6
  2. package/lib/commonjs/molecules/Steps.js.map +1 -1
  3. package/lib/commonjs/molecules/SymmetricGrid.js +84 -0
  4. package/lib/commonjs/molecules/SymmetricGrid.js.map +1 -0
  5. package/lib/commonjs/molecules/index.js +7 -0
  6. package/lib/commonjs/molecules/index.js.map +1 -1
  7. package/lib/commonjs/organisms/AlertProvider.js +12 -4
  8. package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
  9. package/lib/commonjs/organisms/BottomSheet.js +58 -50
  10. package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
  11. package/lib/commonjs/organisms/Modal.js +44 -42
  12. package/lib/commonjs/organisms/Modal.js.map +1 -1
  13. package/lib/commonjs/organisms/NavigationBar.js +4 -1
  14. package/lib/commonjs/organisms/NavigationBar.js.map +1 -1
  15. package/lib/commonjs/organisms/ReorderableList.js +143 -0
  16. package/lib/commonjs/organisms/ReorderableList.js.map +1 -0
  17. package/lib/commonjs/organisms/Screen.js +1 -2
  18. package/lib/commonjs/organisms/Screen.js.map +1 -1
  19. package/lib/commonjs/organisms/index.js +7 -0
  20. package/lib/commonjs/organisms/index.js.map +1 -1
  21. package/lib/commonjs/safe-area/SafeAreaUIProvider.js +38 -30
  22. package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -1
  23. package/lib/commonjs/theme/UIProvider.js +35 -8
  24. package/lib/commonjs/theme/UIProvider.js.map +1 -1
  25. package/lib/module/molecules/Steps.js +14 -6
  26. package/lib/module/molecules/Steps.js.map +1 -1
  27. package/lib/module/molecules/SymmetricGrid.js +79 -0
  28. package/lib/module/molecules/SymmetricGrid.js.map +1 -0
  29. package/lib/module/molecules/index.js +1 -0
  30. package/lib/module/molecules/index.js.map +1 -1
  31. package/lib/module/organisms/AlertProvider.js +12 -4
  32. package/lib/module/organisms/AlertProvider.js.map +1 -1
  33. package/lib/module/organisms/BottomSheet.js +59 -51
  34. package/lib/module/organisms/BottomSheet.js.map +1 -1
  35. package/lib/module/organisms/Modal.js +45 -43
  36. package/lib/module/organisms/Modal.js.map +1 -1
  37. package/lib/module/organisms/NavigationBar.js +4 -1
  38. package/lib/module/organisms/NavigationBar.js.map +1 -1
  39. package/lib/module/organisms/ReorderableList.js +137 -0
  40. package/lib/module/organisms/ReorderableList.js.map +1 -0
  41. package/lib/module/organisms/Screen.js +1 -2
  42. package/lib/module/organisms/Screen.js.map +1 -1
  43. package/lib/module/organisms/index.js +1 -0
  44. package/lib/module/organisms/index.js.map +1 -1
  45. package/lib/module/safe-area/SafeAreaUIProvider.js +39 -31
  46. package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -1
  47. package/lib/module/theme/UIProvider.js +36 -10
  48. package/lib/module/theme/UIProvider.js.map +1 -1
  49. package/lib/typescript/molecules/Steps.d.ts.map +1 -1
  50. package/lib/typescript/molecules/SymmetricGrid.d.ts +26 -0
  51. package/lib/typescript/molecules/SymmetricGrid.d.ts.map +1 -0
  52. package/lib/typescript/molecules/index.d.ts +2 -0
  53. package/lib/typescript/molecules/index.d.ts.map +1 -1
  54. package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
  55. package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
  56. package/lib/typescript/organisms/Modal.d.ts.map +1 -1
  57. package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -1
  58. package/lib/typescript/organisms/ReorderableList.d.ts +29 -0
  59. package/lib/typescript/organisms/ReorderableList.d.ts.map +1 -0
  60. package/lib/typescript/organisms/Screen.d.ts.map +1 -1
  61. package/lib/typescript/organisms/index.d.ts +2 -0
  62. package/lib/typescript/organisms/index.d.ts.map +1 -1
  63. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +0 -38
  64. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -1
  65. package/lib/typescript/theme/UIProvider.d.ts +21 -1
  66. package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
  67. package/package.json +11 -1
  68. package/registry/alert-provider.json +1 -1
  69. package/registry/bottom-sheet.json +1 -1
  70. package/registry/core.json +1 -1
  71. package/registry/index.json +31 -7
  72. package/registry/manifest.json +14 -0
  73. package/registry/modal.json +1 -1
  74. package/registry/navigation-bar.json +1 -1
  75. package/registry/reorderable-list.json +18 -0
  76. package/registry/screen.json +1 -1
  77. package/registry/steps.json +1 -1
  78. package/registry/symmetric-grid.json +18 -0
  79. package/src/molecules/Steps.tsx +14 -8
  80. package/src/molecules/SymmetricGrid.tsx +82 -0
  81. package/src/molecules/index.ts +2 -0
  82. package/src/organisms/AlertProvider.tsx +13 -5
  83. package/src/organisms/BottomSheet.tsx +58 -44
  84. package/src/organisms/Modal.tsx +63 -55
  85. package/src/organisms/NavigationBar.tsx +4 -1
  86. package/src/organisms/ReorderableList.tsx +151 -0
  87. package/src/organisms/Screen.tsx +1 -2
  88. package/src/organisms/index.ts +2 -0
  89. package/src/safe-area/SafeAreaUIProvider.tsx +46 -41
  90. package/src/theme/UIProvider.tsx +50 -8
@@ -0,0 +1,151 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { Animated, PanResponder, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+
5
+ export interface ReorderableListProps<T> {
6
+ data: T[];
7
+ keyExtractor: (item: T, index: number) => string;
8
+ /** Alto fijo de cada fila: necesario para calcular el índice destino sin medir layout async. */
9
+ itemHeight: number;
10
+ renderItem: (item: T, index: number, dragging: boolean) => React.ReactElement;
11
+ /** Se llama al soltar, con el arreglo ya reordenado. No muta `data`. */
12
+ onReorder: (data: T[]) => void;
13
+ disabled?: boolean;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ /** Reordena moviendo el ítem de `from` a `to`, sin mutar `data`. Exportada aparte por ser testeable sin simular gestos. */
18
+ export function move<T>(data: T[], from: number, to: number): T[] {
19
+ const next = data.slice();
20
+ const [item] = next.splice(from, 1) as [T];
21
+ next.splice(to, 0, item);
22
+ return next;
23
+ }
24
+
25
+ const useStyles = createStyles(() => ({
26
+ row: { width: '100%' },
27
+ }));
28
+
29
+ /**
30
+ * ReorderableList
31
+ * Ley de destino común (common fate): al arrastrar una fila, las filas que
32
+ * tiene que atravesar se desplazan juntas en el mismo sentido — eso es lo
33
+ * que comunica "van a intercambiar lugar" antes de soltar el dedo, no un
34
+ * simple resaltado del ítem activo.
35
+ *
36
+ * Sin dependencias de gestos/animación externas (reanimated, gesture-handler):
37
+ * PanResponder + Animated alcanzan porque el cálculo de índice destino
38
+ * necesita el valor de arrastre en JS en cada movimiento (no algo que
39
+ * useNativeDriver pueda resolver solo).
40
+ */
41
+ export function ReorderableList<T>({
42
+ data,
43
+ keyExtractor,
44
+ itemHeight,
45
+ renderItem,
46
+ onReorder,
47
+ disabled = false,
48
+ style,
49
+ }: ReorderableListProps<T>) {
50
+ const styles = useStyles();
51
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
52
+ const [targetIndex, setTargetIndex] = useState<number | null>(null);
53
+ const dy = useRef(new Animated.Value(0)).current;
54
+ const activeIndexRef = useRef<number | null>(null);
55
+ // El PanResponder de cada fila se crea una sola vez (se cachea en el Map de
56
+ // abajo) y su closure queda fijo al render de esa primera vez: leer
57
+ // `targetIndex` (state) directamente ahí siempre daría el valor del
58
+ // montaje. Un ref sí refleja la última escritura sin importar cuándo se
59
+ // creó la closure que lo lee.
60
+ const targetIndexRef = useRef<number | null>(null);
61
+ const dataRef = useRef(data);
62
+ dataRef.current = data;
63
+ // Igual razón que targetIndexRef: el responder de cada fila se crea una
64
+ // sola vez, así que leer `disabled` directo del closure congelaría el
65
+ // valor del momento en que esa fila se creó.
66
+ const disabledRef = useRef(disabled);
67
+ disabledRef.current = disabled;
68
+
69
+ const panResponders = useRef(new Map<number, ReturnType<typeof PanResponder.create>>()).current;
70
+ // Si `data` se achica, los responders de índices que ya no existen quedan
71
+ // colgados en el Map para siempre: se podan en cada render.
72
+ for (const index of panResponders.keys()) {
73
+ if (index >= data.length) panResponders.delete(index);
74
+ }
75
+
76
+ function panResponderFor(index: number) {
77
+ let responder = panResponders.get(index);
78
+ if (responder) return responder;
79
+
80
+ responder = PanResponder.create({
81
+ onStartShouldSetPanResponder: () => !disabledRef.current,
82
+ onMoveShouldSetPanResponder: (_, gesture) => !disabledRef.current && Math.abs(gesture.dy) > 2,
83
+ onPanResponderGrant: () => {
84
+ activeIndexRef.current = index;
85
+ targetIndexRef.current = index;
86
+ setActiveIndex(index);
87
+ setTargetIndex(index);
88
+ dy.setValue(0);
89
+ },
90
+ onPanResponderMove: (_, gesture) => {
91
+ dy.setValue(gesture.dy);
92
+ const from = activeIndexRef.current;
93
+ if (from === null) return;
94
+ const rawTarget = from + Math.round(gesture.dy / itemHeight);
95
+ const clamped = Math.max(0, Math.min(dataRef.current.length - 1, rawTarget));
96
+ targetIndexRef.current = clamped;
97
+ setTargetIndex((prev) => (prev === clamped ? prev : clamped));
98
+ },
99
+ onPanResponderRelease: () => {
100
+ const from = activeIndexRef.current;
101
+ const to = targetIndexRef.current;
102
+ dy.setValue(0);
103
+ activeIndexRef.current = null;
104
+ targetIndexRef.current = null;
105
+ setActiveIndex(null);
106
+ setTargetIndex(null);
107
+ if (from === null || to === null || from === to) return;
108
+ onReorder(move(dataRef.current, from, to));
109
+ },
110
+ onPanResponderTerminate: () => {
111
+ dy.setValue(0);
112
+ activeIndexRef.current = null;
113
+ targetIndexRef.current = null;
114
+ setActiveIndex(null);
115
+ setTargetIndex(null);
116
+ },
117
+ });
118
+ panResponders.set(index, responder);
119
+ return responder;
120
+ }
121
+
122
+ return (
123
+ <View style={style}>
124
+ {data.map((item, index) => {
125
+ const isActive = activeIndex === index;
126
+ // Filas entre el origen y el destino se corren un lugar entero en el
127
+ // sentido del arrastre: es el efecto de "grupo que se mueve junto".
128
+ let shift = 0;
129
+ if (activeIndex !== null && targetIndex !== null && !isActive) {
130
+ if (activeIndex < targetIndex && index > activeIndex && index <= targetIndex) shift = -itemHeight;
131
+ else if (activeIndex > targetIndex && index < activeIndex && index >= targetIndex) shift = itemHeight;
132
+ }
133
+
134
+ return (
135
+ <Animated.View
136
+ key={keyExtractor(item, index)}
137
+ style={[
138
+ styles.row,
139
+ isActive
140
+ ? { transform: [{ translateY: dy }], zIndex: 1, elevation: 4 }
141
+ : { transform: [{ translateY: shift }] },
142
+ ]}
143
+ {...panResponderFor(index).panHandlers}
144
+ >
145
+ {renderItem(item, index, isActive)}
146
+ </Animated.View>
147
+ );
148
+ })}
149
+ </View>
150
+ );
151
+ }
@@ -68,12 +68,11 @@ export const Screen = memo(({
68
68
  {scrollable ? (
69
69
  <ScrollView
70
70
  style={styles.container}
71
- contentInsetAdjustmentBehavior="automatic"
72
71
  automaticallyAdjustKeyboardInsets
73
72
  contentContainerStyle={[
74
73
  styles.contentContainer,
75
74
  { paddingTop: applyTop ? insets.top : 0 },
76
- Platform.OS === 'android' && applyBottom && { paddingBottom: insets.bottom },
75
+ applyBottom && { paddingBottom: insets.bottom },
77
76
  contentContainerStyle,
78
77
  ]}
79
78
  keyboardShouldPersistTaps="handled"
@@ -28,3 +28,5 @@ export { ThemeToggle } from './ThemeToggle';
28
28
  export type { ThemeToggleProps } from './ThemeToggle';
29
29
  export { NavigationBar } from './NavigationBar';
30
30
  export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
31
+ export { ReorderableList } from './ReorderableList';
32
+ export type { ReorderableListProps } from './ReorderableList';
@@ -1,59 +1,64 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+ import type { ViewStyle } from 'react-native';
2
3
  import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
3
- import { UIProvider, type UIProviderProps } from '../theme/UIProvider';
4
+ import {
5
+ UIProvider,
6
+ useInsets,
7
+ type ModalSafeAreaBoundaryProps,
8
+ type UIProviderProps,
9
+ } from '../theme/UIProvider';
10
+ import type { EdgeInsets } from '../theme/insets';
4
11
 
5
12
  export interface SafeAreaUIProviderProps extends Omit<UIProviderProps, 'insets'> {
6
- /**
7
- * Insets explícitos. Ganan sobre los medidos — útil para tests, storybook o
8
- * una pantalla embebida que no ocupa la ventana entera.
9
- */
10
13
  insets?: UIProviderProps['insets'];
11
- /**
12
- * Montar también el `<SafeAreaProvider>`. Ponelo en false si tu app ya tiene
13
- * uno arriba (React Navigation lo monta solo en algunas plantillas):
14
- * un provider anidado mide el frame de *su* View, no el de la ventana, así
15
- * que ahí adentro los insets darían cero.
16
- * @default true
17
- */
18
14
  mountSafeAreaProvider?: boolean;
19
15
  }
20
16
 
17
+ const probeStyle: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
18
+
19
+ function sameInsets(a: EdgeInsets, b: EdgeInsets) {
20
+ return a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left;
21
+ }
22
+
23
+ function InsetsProbe({ onMeasure }: { onMeasure: (insets: EdgeInsets) => void }) {
24
+ const insets = useSafeAreaInsets();
25
+ useEffect(() => {
26
+ onMeasure(insets);
27
+ }, [insets, onMeasure]);
28
+ return null;
29
+ }
30
+
31
+ function RemeasuringModalSafeAreaBoundary({ children }: ModalSafeAreaBoundaryProps) {
32
+ const ambient = useInsets();
33
+ const [measured, setMeasured] = useState<EdgeInsets | null>(null);
34
+ const onMeasure = React.useCallback(
35
+ (next: EdgeInsets) => setMeasured((prev) => (prev && sameInsets(prev, next) ? prev : next)),
36
+ []
37
+ );
38
+
39
+ return (
40
+ <>
41
+ <SafeAreaProvider style={probeStyle} pointerEvents="none">
42
+ <InsetsProbe onMeasure={onMeasure} />
43
+ </SafeAreaProvider>
44
+ {children(measured ?? ambient)}
45
+ </>
46
+ );
47
+ }
48
+
21
49
  function Measured({ insets: override, children, ...rest }: SafeAreaUIProviderProps) {
22
50
  const measured = useSafeAreaInsets();
23
51
  return (
24
- <UIProvider insets={override ?? measured} {...rest}>
52
+ <UIProvider
53
+ insets={override ?? measured}
54
+ ModalSafeAreaBoundary={RemeasuringModalSafeAreaBoundary}
55
+ {...rest}
56
+ >
25
57
  {children}
26
58
  </UIProvider>
27
59
  );
28
60
  }
29
61
 
30
- /**
31
- * SafeAreaUIProvider
32
- * `UIProvider` con los insets ya cableados desde `react-native-safe-area-context`.
33
- *
34
- * Vive en su propio subpath (`orn-ui/safe-area`) a propósito: es el único
35
- * archivo de la librería que importa un paquete de terceros. Nada del entry
36
- * principal lo referencia, así que quien hace `import { Button } from 'orn-ui'`
37
- * nunca lo resuelve y el bundle sigue sin dependencias de runtime.
38
- *
39
- * ```tsx
40
- * import { SafeAreaUIProvider } from 'orn-ui/safe-area';
41
- *
42
- * export default function App() {
43
- * return (
44
- * <SafeAreaUIProvider defaultMode="system">
45
- * <RootNavigator />
46
- * </SafeAreaUIProvider>
47
- * );
48
- * }
49
- * ```
50
- *
51
- * El equivalente a mano — pasarle `insets={useSafeAreaInsets()}` a `UIProvider`
52
- * desde un componente que ya esté dentro de `<SafeAreaProvider>` — sigue
53
- * siendo válido. Esto sólo evita el error silencioso de olvidarlo: `insets`
54
- * por defecto es `{0,0,0,0}`, y con ceros el bottom sheet queda pegado contra
55
- * la barra de gestos y el modal `full` se mete abajo del notch.
56
- */
57
62
  export function SafeAreaUIProvider({
58
63
  mountSafeAreaProvider = true,
59
64
  ...props
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';
1
+ import React, { createContext, useContext, useMemo, useState, useCallback, useRef } from 'react';
2
2
  import { Appearance, useColorScheme } from 'react-native';
3
3
  import { defaultTheme } from './createTheme';
4
4
  import { defaultLabels, type Labels } from './labels';
@@ -9,6 +9,24 @@ import type { IconRenderer } from '../icons/types';
9
9
 
10
10
  export type ThemeMode = 'system' | 'light' | 'dark';
11
11
 
12
+ /**
13
+ * Un `<Modal>`/`<BottomSheet>` nativo abre su propia ventana (en Android
14
+ * siempre; en iOS según `presentationStyle`), así que los insets medidos en
15
+ * la raíz de la app pueden no valer ahí adentro. Este boundary es el punto de
16
+ * inyección: el default sólo repite los insets ambientes (comportamiento de
17
+ * siempre, cero deps), pero `SafeAreaUIProvider` lo reemplaza por uno que
18
+ * monta un `<SafeAreaProvider>` nuevo y remide dentro de esa ventana.
19
+ */
20
+ export interface ModalSafeAreaBoundaryProps {
21
+ children: (insets: EdgeInsets) => React.ReactNode;
22
+ }
23
+ export type ModalSafeAreaBoundaryComponent = React.ComponentType<ModalSafeAreaBoundaryProps>;
24
+
25
+ function DefaultModalSafeAreaBoundary({ children }: ModalSafeAreaBoundaryProps) {
26
+ const insets = useInsets();
27
+ return <>{children(insets)}</>;
28
+ }
29
+
12
30
  interface UIContextValue {
13
31
  theme: Theme;
14
32
  mode: ThemeMode;
@@ -17,13 +35,11 @@ interface UIContextValue {
17
35
  insets: EdgeInsets;
18
36
  labels: Labels;
19
37
  allowFontScaling: boolean;
38
+ ModalSafeAreaBoundary: ModalSafeAreaBoundaryComponent;
20
39
  }
21
40
 
22
41
  const UIContext = createContext<UIContextValue | undefined>(undefined);
23
42
 
24
- /** Una sola vez por sesión: en un re-render el aviso no aporta nada nuevo. */
25
- let warnedAboutInsets = false;
26
-
27
43
  export interface UIProviderProps {
28
44
  children: React.ReactNode;
29
45
  /** Par light/dark. Por defecto el theme de marca de la librería (créalo con createTheme()). */
@@ -40,6 +56,12 @@ export interface UIProviderProps {
40
56
  labels?: Partial<Labels>;
41
57
  /** @default false, para paridad con el resto de la librería (accesibilidad primero). */
42
58
  allowFontScaling?: boolean;
59
+ /**
60
+ * Fuente de insets que usan `Modal`/`BottomSheet` para su propia ventana
61
+ * nativa. No la pases a mano — `SafeAreaUIProvider` la cablea sola.
62
+ * @default repite los insets ambientes (comportamiento sin safe-area-context)
63
+ */
64
+ ModalSafeAreaBoundary?: ModalSafeAreaBoundaryComponent;
43
65
  }
44
66
 
45
67
  /**
@@ -59,6 +81,7 @@ export function UIProvider({
59
81
  insets = zeroInsets,
60
82
  labels: labelOverrides,
61
83
  allowFontScaling = false,
84
+ ModalSafeAreaBoundary = DefaultModalSafeAreaBoundary,
62
85
  }: UIProviderProps) {
63
86
  const systemScheme = useColorScheme();
64
87
  const [uncontrolledMode, setUncontrolledMode] = useState<ThemeMode>(defaultMode);
@@ -73,11 +96,16 @@ export function UIProvider({
73
96
  [isControlled, onModeChange]
74
97
  );
75
98
 
99
+ // Por instancia, no por módulo: dos `<UIProvider>` en el mismo árbol (tests,
100
+ // un modal con su propio provider) avisan cada uno si le faltan insets, en
101
+ // vez de que el segundo se calle porque el primero ya avisó una vez.
102
+ const warnedAboutInsets = useRef(false);
103
+
76
104
  // Olvidar `insets` no rompe nada visible al montar: el default de ceros
77
105
  // recién se nota cuando abrís un Modal `full` (se mete abajo del notch) o un
78
106
  // BottomSheet (queda pegado a la barra de gestos). El aviso llega antes.
79
- if (__DEV__ && insets === zeroInsets && !warnedAboutInsets) {
80
- warnedAboutInsets = true;
107
+ if (__DEV__ && insets === zeroInsets && !warnedAboutInsets.current) {
108
+ warnedAboutInsets.current = true;
81
109
  console.warn(
82
110
  'orn-ui: <UIProvider> sin `insets`, se usan {top:0,bottom:0,left:0,right:0}. ' +
83
111
  'Los modales y el Screen quedan sin safe area. Pasá `insets={useSafeAreaInsets()}` ' +
@@ -96,8 +124,17 @@ export function UIProvider({
96
124
  );
97
125
 
98
126
  const value = useMemo<UIContextValue>(
99
- () => ({ theme: resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling }),
100
- [resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling]
127
+ () => ({
128
+ theme: resolvedTheme,
129
+ mode,
130
+ setMode,
131
+ icons,
132
+ insets,
133
+ labels,
134
+ allowFontScaling,
135
+ ModalSafeAreaBoundary,
136
+ }),
137
+ [resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling, ModalSafeAreaBoundary]
101
138
  );
102
139
 
103
140
  return <UIContext.Provider value={value}>{children}</UIContext.Provider>;
@@ -140,5 +177,10 @@ export function useAllowFontScaling(): boolean {
140
177
  return useUIContext().allowFontScaling;
141
178
  }
142
179
 
180
+ /** Interno: lo consumen `Modal`/`BottomSheet` para remedir insets dentro de su propia ventana nativa. */
181
+ export function useModalSafeAreaBoundary(): ModalSafeAreaBoundaryComponent {
182
+ return useUIContext().ModalSafeAreaBoundary;
183
+ }
184
+
143
185
  /** Re-exportado para que tests/consumidores puedan simular cambios de sistema sin RN real. */
144
186
  export const __testUtils = { Appearance };