@oxyhq/bloom 0.8.2 → 0.8.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 (140) hide show
  1. package/lib/commonjs/avatar/Avatar.js +2 -1
  2. package/lib/commonjs/avatar/Avatar.js.map +1 -1
  3. package/lib/commonjs/badge/Badge.js +2 -1
  4. package/lib/commonjs/badge/Badge.js.map +1 -1
  5. package/lib/commonjs/bottom-sheet/index.js +3 -2
  6. package/lib/commonjs/bottom-sheet/index.js.map +1 -1
  7. package/lib/commonjs/context-menu/index.web.js +5 -2
  8. package/lib/commonjs/context-menu/index.web.js.map +1 -1
  9. package/lib/commonjs/dialog/CenteredDialog.web.js +4 -2
  10. package/lib/commonjs/dialog/CenteredDialog.web.js.map +1 -1
  11. package/lib/commonjs/dialog/Dialog.web.js +4 -2
  12. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  13. package/lib/commonjs/dialog/SheetShell.js +2 -1
  14. package/lib/commonjs/dialog/SheetShell.js.map +1 -1
  15. package/lib/commonjs/index.js +14 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/index.web.js +14 -0
  18. package/lib/commonjs/index.web.js.map +1 -1
  19. package/lib/commonjs/menu/index.web.js +95 -29
  20. package/lib/commonjs/menu/index.web.js.map +1 -1
  21. package/lib/commonjs/popover/index.web.js +5 -2
  22. package/lib/commonjs/popover/index.web.js.map +1 -1
  23. package/lib/commonjs/portal/index.js +2 -1
  24. package/lib/commonjs/portal/index.js.map +1 -1
  25. package/lib/commonjs/portal/index.web.js +2 -1
  26. package/lib/commonjs/portal/index.web.js.map +1 -1
  27. package/lib/commonjs/prompt-input/PromptInput.js +4 -3
  28. package/lib/commonjs/prompt-input/PromptInput.js.map +1 -1
  29. package/lib/commonjs/select/index.web.js +5 -2
  30. package/lib/commonjs/select/index.web.js.map +1 -1
  31. package/lib/commonjs/styles/index.js +13 -0
  32. package/lib/commonjs/styles/index.js.map +1 -1
  33. package/lib/commonjs/styles/z-index.js +52 -0
  34. package/lib/commonjs/styles/z-index.js.map +1 -0
  35. package/lib/commonjs/tooltip/index.web.js +2 -1
  36. package/lib/commonjs/tooltip/index.web.js.map +1 -1
  37. package/lib/module/avatar/Avatar.js +2 -1
  38. package/lib/module/avatar/Avatar.js.map +1 -1
  39. package/lib/module/badge/Badge.js +2 -1
  40. package/lib/module/badge/Badge.js.map +1 -1
  41. package/lib/module/bottom-sheet/index.js +3 -2
  42. package/lib/module/bottom-sheet/index.js.map +1 -1
  43. package/lib/module/context-menu/index.web.js +6 -3
  44. package/lib/module/context-menu/index.web.js.map +1 -1
  45. package/lib/module/dialog/CenteredDialog.web.js +4 -2
  46. package/lib/module/dialog/CenteredDialog.web.js.map +1 -1
  47. package/lib/module/dialog/Dialog.web.js +4 -2
  48. package/lib/module/dialog/Dialog.web.js.map +1 -1
  49. package/lib/module/dialog/SheetShell.js +2 -1
  50. package/lib/module/dialog/SheetShell.js.map +1 -1
  51. package/lib/module/index.js +1 -1
  52. package/lib/module/index.js.map +1 -1
  53. package/lib/module/index.web.js +1 -1
  54. package/lib/module/index.web.js.map +1 -1
  55. package/lib/module/menu/index.web.js +97 -31
  56. package/lib/module/menu/index.web.js.map +1 -1
  57. package/lib/module/popover/index.web.js +4 -2
  58. package/lib/module/popover/index.web.js.map +1 -1
  59. package/lib/module/portal/index.js +2 -1
  60. package/lib/module/portal/index.js.map +1 -1
  61. package/lib/module/portal/index.web.js +2 -1
  62. package/lib/module/portal/index.web.js.map +1 -1
  63. package/lib/module/prompt-input/PromptInput.js +4 -3
  64. package/lib/module/prompt-input/PromptInput.js.map +1 -1
  65. package/lib/module/select/index.web.js +4 -2
  66. package/lib/module/select/index.web.js.map +1 -1
  67. package/lib/module/styles/index.js +1 -0
  68. package/lib/module/styles/index.js.map +1 -1
  69. package/lib/module/styles/z-index.js +45 -0
  70. package/lib/module/styles/z-index.js.map +1 -0
  71. package/lib/module/tooltip/index.web.js +2 -1
  72. package/lib/module/tooltip/index.web.js.map +1 -1
  73. package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/badge/Badge.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/context-menu/index.web.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/dialog/SheetShell.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/index.d.ts +1 -1
  81. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/index.web.d.ts +1 -1
  83. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/menu/types.d.ts +11 -0
  86. package/lib/typescript/commonjs/menu/types.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/popover/index.web.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/portal/index.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/portal/index.web.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/prompt-input/PromptInput.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/styles/index.d.ts +1 -0
  93. package/lib/typescript/commonjs/styles/index.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/styles/z-index.d.ts +49 -0
  95. package/lib/typescript/commonjs/styles/z-index.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/tooltip/index.web.d.ts.map +1 -1
  97. package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
  98. package/lib/typescript/module/badge/Badge.d.ts.map +1 -1
  99. package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
  100. package/lib/typescript/module/context-menu/index.web.d.ts.map +1 -1
  101. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +1 -1
  102. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  103. package/lib/typescript/module/dialog/SheetShell.d.ts.map +1 -1
  104. package/lib/typescript/module/index.d.ts +1 -1
  105. package/lib/typescript/module/index.d.ts.map +1 -1
  106. package/lib/typescript/module/index.web.d.ts +1 -1
  107. package/lib/typescript/module/index.web.d.ts.map +1 -1
  108. package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
  109. package/lib/typescript/module/menu/types.d.ts +11 -0
  110. package/lib/typescript/module/menu/types.d.ts.map +1 -1
  111. package/lib/typescript/module/popover/index.web.d.ts.map +1 -1
  112. package/lib/typescript/module/portal/index.d.ts.map +1 -1
  113. package/lib/typescript/module/portal/index.web.d.ts.map +1 -1
  114. package/lib/typescript/module/prompt-input/PromptInput.d.ts.map +1 -1
  115. package/lib/typescript/module/select/index.web.d.ts.map +1 -1
  116. package/lib/typescript/module/styles/index.d.ts +1 -0
  117. package/lib/typescript/module/styles/index.d.ts.map +1 -1
  118. package/lib/typescript/module/styles/z-index.d.ts +49 -0
  119. package/lib/typescript/module/styles/z-index.d.ts.map +1 -0
  120. package/lib/typescript/module/tooltip/index.web.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/avatar/Avatar.tsx +2 -1
  123. package/src/badge/Badge.tsx +2 -1
  124. package/src/bottom-sheet/index.tsx +3 -2
  125. package/src/context-menu/index.web.tsx +5 -2
  126. package/src/dialog/CenteredDialog.web.tsx +4 -2
  127. package/src/dialog/Dialog.web.tsx +4 -2
  128. package/src/dialog/SheetShell.tsx +2 -1
  129. package/src/index.ts +1 -1
  130. package/src/index.web.ts +1 -1
  131. package/src/menu/index.web.tsx +127 -40
  132. package/src/menu/types.ts +12 -0
  133. package/src/popover/index.web.tsx +5 -2
  134. package/src/portal/index.tsx +2 -1
  135. package/src/portal/index.web.tsx +2 -1
  136. package/src/prompt-input/PromptInput.tsx +4 -3
  137. package/src/select/index.web.tsx +5 -2
  138. package/src/styles/index.ts +1 -0
  139. package/src/styles/z-index.ts +47 -0
  140. package/src/tooltip/index.web.tsx +2 -1
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared stacking scale for Bloom primitives.
3
+ *
4
+ * Keep component layers on this scale instead of introducing local magic
5
+ * numbers. App-specific surfaces can still sit above Bloom's inline layers,
6
+ * while portaled overlays use `portalRoot` as the document-level host.
7
+ */
8
+ export declare const Z_INDEX: {
9
+ readonly base: 0;
10
+ readonly raised: 1;
11
+ readonly floating: 10;
12
+ readonly sticky: 20;
13
+ readonly dropdown: 40;
14
+ readonly dropdownSurface: 41;
15
+ readonly overlayBackdrop: 50;
16
+ readonly overlaySurface: 60;
17
+ readonly tooltip: 70;
18
+ readonly toast: 80;
19
+ readonly fullscreen: 90;
20
+ readonly fullscreenControl: 91;
21
+ readonly sheetHandle: 100;
22
+ readonly portalRoot: 999999;
23
+ };
24
+ export declare const Z_INDEX_LAYER_STEP = 10;
25
+ export declare function createDropdownZIndex(layer?: number): {
26
+ readonly root: number;
27
+ readonly surface: number;
28
+ };
29
+ export declare function createOverlayZIndex(layer?: number): {
30
+ readonly backdrop: number;
31
+ readonly surface: number;
32
+ };
33
+ export declare const zIndex: {
34
+ readonly base: 0;
35
+ readonly raised: 1;
36
+ readonly floating: 10;
37
+ readonly sticky: 20;
38
+ readonly dropdown: 40;
39
+ readonly dropdownSurface: 41;
40
+ readonly overlayBackdrop: 50;
41
+ readonly overlaySurface: 60;
42
+ readonly tooltip: 70;
43
+ readonly toast: 80;
44
+ readonly fullscreen: 90;
45
+ readonly fullscreenControl: 91;
46
+ readonly sheetHandle: 100;
47
+ readonly portalRoot: 999999;
48
+ };
49
+ //# sourceMappingURL=z-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"z-index.d.ts","sourceRoot":"","sources":["../../../../src/styles/z-index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;CAeV,CAAC;AAEX,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAMrC,wBAAgB,oBAAoB,CAAC,KAAK,SAAI;;;EAM7C;AAED,wBAAgB,mBAAmB,CAAC,KAAK,SAAI;;;EAM5C;AAED,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;CAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/tooltip/index.web.tsx"],"names":[],"mappings":"AAyBA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CAEA;yBANe,mBAAmB;;;AASnC,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,QAAmB,EACnB,OAAO,EACP,eAAe,GAChB,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C,2CAkBA;AAED,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAMjE;AAED,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf,kDAgEA;AAED,eAAO,MAAM,UAAU;;6CAA4B,CAAC"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/tooltip/index.web.tsx"],"names":[],"mappings":"AA0BA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CAEA;yBANe,mBAAmB;;;AASnC,wBAAgB,KAAK,CAAC,EACpB,QAAQ,EACR,QAAmB,EACnB,OAAO,EACP,eAAe,GAChB,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C,2CAkBA;AAED,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAMjE;AAED,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf,kDAgEA;AAED,eAAO,MAAM,UAAU;;6CAA4B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -4,6 +4,7 @@ import type { TextStyle } from 'react-native';
4
4
 
5
5
  import { useTheme } from '../theme/use-theme';
6
6
  import { useImageResolver } from '../image-resolver/context';
7
+ import { Z_INDEX } from '../styles/z-index';
7
8
  import { lazyRequire } from '../utils/lazy-require';
8
9
  import { useAvatarPlaceholder } from './placeholder-context';
9
10
  import type { AvatarProps } from './types';
@@ -308,7 +309,7 @@ const styles = StyleSheet.create({
308
309
  right: -2,
309
310
  justifyContent: 'center',
310
311
  alignItems: 'center',
311
- zIndex: 1,
312
+ zIndex: Z_INDEX.raised,
312
313
  },
313
314
  errorDetector: {
314
315
  position: 'absolute',
@@ -2,6 +2,7 @@ import React, { memo, useMemo } from 'react';
2
2
  import { View, Text, type ViewStyle, type TextStyle } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
+ import { Z_INDEX } from '../styles/z-index';
5
6
  import type { BadgeProps, BadgeColor, BadgeVariant } from './types';
6
7
 
7
8
  const SIZE_CONFIG = {
@@ -135,7 +136,7 @@ const BadgeComponent: React.FC<BadgeProps> = ({
135
136
  style={[
136
137
  {
137
138
  position: 'absolute',
138
- zIndex: 1,
139
+ zIndex: Z_INDEX.raised,
139
140
  ...PLACEMENT_CONFIG[placement],
140
141
  },
141
142
  badgeStyle,
@@ -11,6 +11,7 @@ import {
11
11
  type StyleProp,
12
12
  } from 'react-native';
13
13
  import { Gesture, GestureDetector, GestureHandlerRootView, type GestureType } from 'react-native-gesture-handler';
14
+ import { Z_INDEX } from '../styles/z-index';
14
15
  import Animated, {
15
16
  interpolate,
16
17
  runOnJS,
@@ -764,7 +765,7 @@ const styles = StyleSheet.create({
764
765
  width: 36,
765
766
  height: 5,
766
767
  borderRadius: 3,
767
- zIndex: 100,
768
+ zIndex: Z_INDEX.sheetHandle,
768
769
  },
769
770
  /**
770
771
  * Hit area for the drag handle in `manualActivation` mode. Absolutely
@@ -781,7 +782,7 @@ const styles = StyleSheet.create({
781
782
  alignItems: 'center',
782
783
  justifyContent: 'flex-start',
783
784
  paddingTop: 6,
784
- zIndex: 100,
785
+ zIndex: Z_INDEX.sheetHandle,
785
786
  },
786
787
  background: {
787
788
  ...StyleSheet.absoluteFill,
@@ -19,6 +19,7 @@ import { useTheme } from '../theme/use-theme';
19
19
  import { Text } from '../typography';
20
20
  import { Portal } from '../portal/index.web';
21
21
  import { useInteractionState } from '../hooks/useInteractionState';
22
+ import { createOverlayZIndex } from '../styles/z-index';
22
23
  import { ItemCtx, useItemContext } from './context';
23
24
  import type {
24
25
  ContextMenuContextValue,
@@ -31,6 +32,8 @@ import type {
31
32
  TriggerProps,
32
33
  } from './types';
33
34
 
35
+ const contextMenuZIndex = createOverlayZIndex();
36
+
34
37
  // ---------------------------------------------------------------------------
35
38
  // Web-specific context (extends base with position)
36
39
  // ---------------------------------------------------------------------------
@@ -327,13 +330,13 @@ const styles = StyleSheet.create({
327
330
  left: 0,
328
331
  right: 0,
329
332
  bottom: 0,
330
- zIndex: 50,
333
+ zIndex: contextMenuZIndex.backdrop,
331
334
  // Opt back in from the Portal root's `pointer-events: none`.
332
335
  pointerEvents: 'auto',
333
336
  },
334
337
  dropdown: {
335
338
  position: 'fixed' as 'absolute',
336
- zIndex: 60,
339
+ zIndex: contextMenuZIndex.surface,
337
340
  borderRadius: 8,
338
341
  padding: 4,
339
342
  borderWidth: 1,
@@ -5,6 +5,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
5
5
  import { useInteractionStates } from '../hooks/useInteractionState';
6
6
  import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '../icons/Times';
7
7
  import { Portal } from '../portal/index.web';
8
+ import { createOverlayZIndex } from '../styles/z-index';
8
9
  import { useTheme } from '../theme/use-theme';
9
10
  import { Text } from '../typography';
10
11
  import type { CenteredDialogProps } from './centered-dialog-types';
@@ -18,6 +19,7 @@ import {
18
19
 
19
20
  const FADE_OUT_DURATION = 150;
20
21
  const CLOSE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
22
+ const dialogZIndex = createOverlayZIndex();
21
23
 
22
24
  /** Stable testID for the dimmed backdrop. Overridden when a `testID` is set. */
23
25
  export const CENTERED_DIALOG_BACKDROP_TESTID = 'bloom-centered-dialog-backdrop';
@@ -312,7 +314,7 @@ const styles = StyleSheet.create({
312
314
  left: 0,
313
315
  right: 0,
314
316
  bottom: 0,
315
- zIndex: 50,
317
+ zIndex: dialogZIndex.backdrop,
316
318
  alignItems: 'center',
317
319
  justifyContent: 'center',
318
320
  pointerEvents: 'auto',
@@ -333,7 +335,7 @@ const styles = StyleSheet.create({
333
335
  shadowOpacity: 0.18,
334
336
  shadowRadius: 24,
335
337
  shadowOffset: { width: 0, height: 8 },
336
- zIndex: 60,
338
+ zIndex: dialogZIndex.surface,
337
339
  },
338
340
  header: {
339
341
  flexDirection: 'row',
@@ -20,6 +20,7 @@ import {
20
20
  import { RemoveScrollBar } from 'react-remove-scroll-bar';
21
21
 
22
22
  import { Portal } from '../portal/index.web';
23
+ import { createOverlayZIndex } from '../styles/z-index';
23
24
  import type { ThemeColors } from '../theme/types';
24
25
  import { useTheme } from '../theme/use-theme';
25
26
  import { Context, useDialogContext, useDialogControl } from './context';
@@ -31,6 +32,7 @@ import type {
31
32
  } from './types';
32
33
 
33
34
  const FADE_OUT_DURATION = 150;
35
+ const dialogZIndex = createOverlayZIndex();
34
36
 
35
37
  const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagation();
36
38
 
@@ -143,7 +145,7 @@ export function Dialog({
143
145
  left: 0,
144
146
  right: 0,
145
147
  bottom: 0,
146
- zIndex: 50,
148
+ zIndex: dialogZIndex.backdrop,
147
149
  alignItems: 'center',
148
150
  justifyContent: 'center',
149
151
  paddingHorizontal: 20,
@@ -216,7 +218,7 @@ function DialogPanel({
216
218
  shadowRadius: 30,
217
219
  shadowOffset: { width: 0, height: 4 },
218
220
  padding: 20,
219
- zIndex: 60,
221
+ zIndex: dialogZIndex.surface,
220
222
  },
221
223
  isClosing
222
224
  ? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
@@ -14,6 +14,7 @@ import React, { useCallback, useImperativeHandle, useMemo, useRef } from 'react'
14
14
  import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
15
15
 
16
16
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
17
+ import { Z_INDEX } from '../styles/z-index';
17
18
  import { useTheme } from '../theme/use-theme';
18
19
  import { Context } from './context';
19
20
  import type { DialogControlProps } from './types';
@@ -135,7 +136,7 @@ const styles = StyleSheet.create({
135
136
  position: 'absolute',
136
137
  width: '100%',
137
138
  alignItems: 'center',
138
- zIndex: 10,
139
+ zIndex: Z_INDEX.floating,
139
140
  height: 20,
140
141
  },
141
142
  handleBar: {
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export * from './theme';
3
3
 
4
4
  // Styles & Utilities
5
- export { atoms, flatten } from './styles';
5
+ export { atoms, flatten, Z_INDEX, zIndex } from './styles';
6
6
  export type { ViewStyleProp, TextStyleProp } from './styles';
7
7
  export * as tokens from './styles/tokens';
8
8
  export { web, native, ios, android, platform, select } from './styles/platform';
package/src/index.web.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  export * from './theme';
8
8
 
9
9
  // Styles & Utilities
10
- export { atoms, flatten } from './styles';
10
+ export { atoms, flatten, Z_INDEX, zIndex } from './styles';
11
11
  export type { ViewStyleProp, TextStyleProp } from './styles';
12
12
  export * as tokens from './styles/tokens';
13
13
  export { web, native, ios, android, platform, select } from './styles/platform';
@@ -5,6 +5,7 @@ import React, {
5
5
  useCallback,
6
6
  useEffect,
7
7
  useId,
8
+ useLayoutEffect,
8
9
  useMemo,
9
10
  useRef,
10
11
  useState,
@@ -13,8 +14,9 @@ import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'rea
13
14
 
14
15
  import { useTheme } from '../theme/use-theme';
15
16
  import { Text } from '../typography';
16
- import { Portal } from '../portal/index.web';
17
17
  import type { DialogControlProps } from '../dialog/types';
18
+ import { Portal } from '../portal/index.web';
19
+ import { createDropdownZIndex } from '../styles/z-index';
18
20
  import {
19
21
  MenuContext,
20
22
  ItemContext,
@@ -32,6 +34,10 @@ import type {
32
34
 
33
35
  export { useMenuContext };
34
36
 
37
+ const menuZIndex = createDropdownZIndex();
38
+ const VIEWPORT_GUTTER = 8;
39
+ const MENU_OFFSET = 6;
40
+
35
41
  export type MenuControlProps = {
36
42
  id: string;
37
43
  isOpen: boolean;
@@ -95,25 +101,58 @@ export function Root({
95
101
  const defaultControl = useMenuControl();
96
102
  const activeControl = control ?? defaultControl;
97
103
  const dialogControl = useMenuControlAsDialogControl(activeControl);
104
+ const rootRef = useRef<View>(null);
105
+ const triggerRef = useRef<unknown>(null);
106
+ const dropdownRef = useRef<unknown>(null);
98
107
 
99
108
  const context = useMemo<MenuContextType>(
100
- () => ({ control: dialogControl, isOpen: activeControl.isOpen }),
109
+ () => ({
110
+ control: dialogControl,
111
+ isOpen: activeControl.isOpen,
112
+ triggerRef,
113
+ dropdownRef,
114
+ }),
101
115
  [activeControl.isOpen, dialogControl],
102
116
  );
103
117
 
118
+ useEffect(() => {
119
+ if (!activeControl.isOpen || typeof document === 'undefined') {
120
+ return undefined;
121
+ }
122
+
123
+ const handlePointerDown = (event: PointerEvent) => {
124
+ const rootNode = rootRef.current as unknown as HTMLElement | null;
125
+ const dropdownNode = dropdownRef.current as HTMLElement | null;
126
+ const eventTarget = event.target as Node;
127
+ if (rootNode?.contains(eventTarget) || dropdownNode?.contains(eventTarget)) {
128
+ return;
129
+ }
130
+ activeControl.close();
131
+ };
132
+
133
+ const handleKeyDown = (event: KeyboardEvent) => {
134
+ if (event.key === 'Escape') {
135
+ activeControl.close();
136
+ }
137
+ };
138
+
139
+ document.addEventListener('pointerdown', handlePointerDown, true);
140
+ document.addEventListener('keydown', handleKeyDown);
141
+
142
+ return () => {
143
+ document.removeEventListener('pointerdown', handlePointerDown, true);
144
+ document.removeEventListener('keydown', handleKeyDown);
145
+ };
146
+ }, [activeControl]);
147
+
104
148
  return (
105
149
  <MenuContext.Provider value={context}>
106
- {activeControl.isOpen && (
107
- <Portal>
108
- <Pressable
109
- style={styles.backdrop}
110
- onPress={() => activeControl.close()}
111
- accessibilityHint=""
112
- accessibilityLabel="Close menu"
113
- />
114
- </Portal>
115
- )}
116
- {children}
150
+ <View
151
+ ref={rootRef}
152
+ style={[styles.root, activeControl.isOpen && styles.openRoot]}
153
+ >
154
+ {children}
155
+ </View>
117
156
  </MenuContext.Provider>
118
157
  );
119
158
  }
@@ -124,7 +163,7 @@ export function Trigger({
124
163
  role = 'button',
125
164
  hint,
126
165
  }: TriggerProps) {
127
- const { control } = useMenuContext();
166
+ const { control, triggerRef } = useMenuContext();
128
167
  const [hovered, setHovered] = useState(false);
129
168
  const [focused, setFocused] = useState(false);
130
169
 
@@ -136,6 +175,7 @@ export function Trigger({
136
175
  pressed: false,
137
176
  },
138
177
  props: {
178
+ ref: triggerRef,
139
179
  onPress: () => control.open(),
140
180
  onFocus: () => setFocused(true),
141
181
  onBlur: () => setFocused(false),
@@ -155,29 +195,76 @@ export function Outer({
155
195
  }>) {
156
196
  const theme = useTheme();
157
197
  const context = useMenuContext();
158
- const dropdownRef = useRef<View>(null);
198
+ const [position, setPosition] = useState<ViewStyle | null>(null);
199
+
200
+ useLayoutEffect(() => {
201
+ if (!context.isOpen || typeof window === 'undefined') {
202
+ return;
203
+ }
204
+
205
+ const triggerNode = context.triggerRef?.current as HTMLElement | null;
206
+ if (!triggerNode?.getBoundingClientRect) {
207
+ return;
208
+ }
209
+
210
+ const updatePosition = () => {
211
+ const rect = triggerNode.getBoundingClientRect();
212
+ const width = Math.max(180, rect.width);
213
+ const availableRight = window.innerWidth - VIEWPORT_GUTTER;
214
+ const left = Math.min(
215
+ Math.max(VIEWPORT_GUTTER, rect.right - width),
216
+ Math.max(VIEWPORT_GUTTER, availableRight - width),
217
+ );
218
+ const top = Math.min(
219
+ rect.bottom + MENU_OFFSET,
220
+ Math.max(VIEWPORT_GUTTER, window.innerHeight - VIEWPORT_GUTTER),
221
+ );
222
+
223
+ setPosition({
224
+ position: 'fixed' as 'absolute',
225
+ top,
226
+ left,
227
+ right: undefined,
228
+ bottom: undefined,
229
+ minWidth: width,
230
+ });
231
+ };
232
+
233
+ updatePosition();
234
+ window.addEventListener('resize', updatePosition);
235
+ window.addEventListener('scroll', updatePosition, true);
236
+
237
+ return () => {
238
+ window.removeEventListener('resize', updatePosition);
239
+ window.removeEventListener('scroll', updatePosition, true);
240
+ };
241
+ }, [context.isOpen, context.triggerRef]);
159
242
 
160
243
  if (!context.isOpen) {
161
244
  return null;
162
245
  }
163
246
 
164
247
  return (
165
- <View
166
- ref={dropdownRef}
167
- style={[
168
- styles.dropdown,
169
- {
170
- backgroundColor: theme.isDark
171
- ? theme.colors.backgroundSecondary
172
- : theme.colors.background,
173
- borderColor: theme.colors.borderLight,
174
- shadowColor: theme.colors.shadow,
175
- },
176
- style,
177
- ]}
178
- >
179
- {children}
180
- </View>
248
+ <Portal>
249
+ <View
250
+ ref={context.dropdownRef as React.Ref<View>}
251
+ style={[
252
+ styles.dropdown,
253
+ {
254
+ backgroundColor: theme.isDark
255
+ ? theme.colors.backgroundSecondary
256
+ : theme.colors.background,
257
+ borderColor: theme.colors.borderLight,
258
+ shadowColor: theme.colors.shadow,
259
+ },
260
+ style,
261
+ styles.portaledDropdown,
262
+ position,
263
+ ]}
264
+ >
265
+ {children}
266
+ </View>
267
+ </Portal>
181
268
  );
182
269
  }
183
270
 
@@ -294,26 +381,26 @@ export function Divider() {
294
381
  }
295
382
 
296
383
  const styles = StyleSheet.create({
297
- backdrop: {
298
- position: 'fixed' as 'absolute',
299
- top: 0,
300
- left: 0,
301
- right: 0,
302
- bottom: 0,
303
- zIndex: 50,
304
- // Opt back in from the Portal root's `pointer-events: none`.
305
- pointerEvents: 'auto',
384
+ root: {
385
+ position: 'relative',
386
+ },
387
+ openRoot: {
388
+ zIndex: menuZIndex.root,
306
389
  },
307
390
  dropdown: {
308
391
  borderRadius: 8,
309
392
  padding: 4,
310
393
  borderWidth: 1,
311
394
  overflow: 'hidden',
395
+ zIndex: menuZIndex.surface,
312
396
  shadowOpacity: 0.15,
313
397
  shadowRadius: 12,
314
398
  shadowOffset: { width: 0, height: 4 },
315
399
  minWidth: 180,
316
400
  },
401
+ portaledDropdown: {
402
+ pointerEvents: 'auto',
403
+ },
317
404
  webItem: {
318
405
  flexDirection: 'row',
319
406
  alignItems: 'center',
package/src/menu/types.ts CHANGED
@@ -10,9 +10,20 @@ import type {
10
10
  import type { Props as SVGIconProps } from '../icons/common';
11
11
  import type { DialogControlProps } from '../dialog/types';
12
12
 
13
+ export type MenuAnchorRect = {
14
+ top: number;
15
+ left: number;
16
+ right: number;
17
+ bottom: number;
18
+ width: number;
19
+ height: number;
20
+ };
21
+
13
22
  export type MenuContextType = {
14
23
  control: DialogControlProps;
15
24
  isOpen?: boolean;
25
+ triggerRef?: React.RefObject<unknown>;
26
+ dropdownRef?: React.RefObject<unknown>;
16
27
  };
17
28
 
18
29
  export type ItemContextType = {
@@ -27,6 +38,7 @@ export type TriggerChildProps = {
27
38
  pressed: boolean;
28
39
  };
29
40
  props: {
41
+ ref?: React.Ref<unknown>;
30
42
  onPress: () => void;
31
43
  onFocus?: () => void;
32
44
  onBlur?: () => void;
@@ -11,6 +11,7 @@ import { Pressable, StyleSheet, View } from 'react-native';
11
11
 
12
12
  import { useTheme } from '../theme/use-theme';
13
13
  import { Portal } from '../portal/index.web';
14
+ import { createOverlayZIndex } from '../styles/z-index';
14
15
  import { PopoverContext, usePopoverContext } from './context';
15
16
  import type {
16
17
  PopoverContentProps,
@@ -32,6 +33,8 @@ export type {
32
33
 
33
34
  export { usePopoverContext };
34
35
 
36
+ const popoverZIndex = createOverlayZIndex();
37
+
35
38
  /**
36
39
  * Web self-contained control. Unlike native (which proxies a `BottomSheet`
37
40
  * via `useDialogControl`), the web popover owns its own open state and
@@ -307,12 +310,12 @@ const styles = StyleSheet.create({
307
310
  left: 0,
308
311
  right: 0,
309
312
  bottom: 0,
310
- zIndex: 50,
313
+ zIndex: popoverZIndex.backdrop,
311
314
  pointerEvents: 'auto',
312
315
  },
313
316
  panel: {
314
317
  position: 'fixed' as 'absolute',
315
- zIndex: 60,
318
+ zIndex: popoverZIndex.surface,
316
319
  borderRadius: 12,
317
320
  borderWidth: 1,
318
321
  paddingVertical: 4,
@@ -11,6 +11,7 @@ import React, {
11
11
  useState,
12
12
  } from 'react';
13
13
  import { View, StyleSheet, Platform } from 'react-native';
14
+ import { Z_INDEX } from '../styles/z-index';
14
15
 
15
16
  type Component = React.ReactElement;
16
17
 
@@ -120,6 +121,6 @@ const styles = StyleSheet.create({
120
121
  right: 0,
121
122
  bottom: 0,
122
123
  pointerEvents: 'box-none',
123
- zIndex: 999999,
124
+ zIndex: Z_INDEX.portalRoot,
124
125
  },
125
126
  });
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import React, { Fragment, memo, useLayoutEffect, useState } from 'react';
17
17
  import { createPortal } from 'react-dom';
18
+ import { Z_INDEX } from '../styles/z-index';
18
19
 
19
20
  /**
20
21
  * Render children into a stable container appended to `document.body`.
@@ -52,7 +53,7 @@ function getPortalRoot(): HTMLElement | null {
52
53
  root.style.pointerEvents = 'none';
53
54
  // Above the document flow; individual portaled components can use
54
55
  // their own z-index for stacking among themselves.
55
- root.style.zIndex = '999999';
56
+ root.style.zIndex = String(Z_INDEX.portalRoot);
56
57
  document.body.appendChild(root);
57
58
  }
58
59
  return root;
@@ -11,6 +11,7 @@ import {
11
11
 
12
12
  import { useTheme } from '../theme/use-theme';
13
13
  import { Portal } from '../portal';
14
+ import { Z_INDEX } from '../styles/z-index';
14
15
  import { PromptInputContext, type Attachment } from './context';
15
16
  import { PromptInputTextarea } from './Textarea';
16
17
  import { PromptInputActions } from './Actions';
@@ -198,7 +199,7 @@ export function PromptInput({
198
199
  position: 'absolute',
199
200
  top: 8,
200
201
  right: 8,
201
- zIndex: 10,
202
+ zIndex: Z_INDEX.floating,
202
203
  backgroundColor: theme.colors.background,
203
204
  borderRadius: 9999,
204
205
  padding: 6,
@@ -233,7 +234,7 @@ export function PromptInput({
233
234
  style={[
234
235
  StyleSheet.absoluteFill,
235
236
  {
236
- zIndex: 9998,
237
+ zIndex: Z_INDEX.fullscreen,
237
238
  backgroundColor: theme.colors.background,
238
239
  // Opt back in from the Portal root's `pointer-events: none`
239
240
  // (web only — harmless on native).
@@ -247,7 +248,7 @@ export function PromptInput({
247
248
  position: 'absolute',
248
249
  top: 16,
249
250
  right: 16,
250
- zIndex: 50,
251
+ zIndex: Z_INDEX.fullscreenControl,
251
252
  padding: 8,
252
253
  backgroundColor: theme.colors.background,
253
254
  borderRadius: 9999,
@@ -4,6 +4,7 @@ import { Pressable, StyleSheet, View } from 'react-native';
4
4
  import { useTheme } from '../theme/use-theme';
5
5
  import { Text } from '../typography';
6
6
  import { Portal } from '../portal/index.web';
7
+ import { createOverlayZIndex } from '../styles/z-index';
7
8
  import { RadioIndicator } from '../radio-indicator';
8
9
  import { useInteractionState } from '../hooks/useInteractionState';
9
10
  import {
@@ -25,6 +26,8 @@ import type {
25
26
 
26
27
  export { useItemContext };
27
28
 
29
+ const selectZIndex = createOverlayZIndex();
30
+
28
31
  // ---------------------------------------------------------------------------
29
32
  // Context
30
33
  // ---------------------------------------------------------------------------
@@ -337,13 +340,13 @@ const styles = StyleSheet.create({
337
340
  left: 0,
338
341
  right: 0,
339
342
  bottom: 0,
340
- zIndex: 50,
343
+ zIndex: selectZIndex.backdrop,
341
344
  // Opt back in from the Portal root's `pointer-events: none`.
342
345
  pointerEvents: 'auto',
343
346
  },
344
347
  dropdown: {
345
348
  position: 'absolute',
346
- zIndex: 60,
349
+ zIndex: selectZIndex.surface,
347
350
  borderRadius: 8,
348
351
  borderWidth: 1,
349
352
  overflow: 'hidden',