@oxyhq/bloom 0.72.2 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/lib/commonjs/avatar-group/AvatarGroup.web.js +22 -21
  2. package/lib/commonjs/avatar-group/AvatarGroup.web.js.map +1 -1
  3. package/lib/commonjs/bottom-sheet/index.web.js +11 -6
  4. package/lib/commonjs/bottom-sheet/index.web.js.map +1 -1
  5. package/lib/commonjs/context-menu/index.web.js +20 -22
  6. package/lib/commonjs/context-menu/index.web.js.map +1 -1
  7. package/lib/commonjs/dialog/Dialog.js +66 -70
  8. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  9. package/lib/commonjs/dialog/Dialog.web.js +49 -51
  10. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  11. package/lib/commonjs/fonts/font-data.web.js +22 -0
  12. package/lib/commonjs/fonts/font-data.web.js.map +1 -0
  13. package/lib/commonjs/menu/index.web.js +15 -11
  14. package/lib/commonjs/menu/index.web.js.map +1 -1
  15. package/lib/commonjs/overlay/index.js +133 -7
  16. package/lib/commonjs/overlay/index.js.map +1 -1
  17. package/lib/commonjs/overlay/stack.js +170 -0
  18. package/lib/commonjs/overlay/stack.js.map +1 -0
  19. package/lib/commonjs/overlay/use-overlay-layer.js +37 -0
  20. package/lib/commonjs/overlay/use-overlay-layer.js.map +1 -0
  21. package/lib/commonjs/popover/index.web.js +31 -34
  22. package/lib/commonjs/popover/index.web.js.map +1 -1
  23. package/lib/commonjs/prompt-input/PromptInput.js +33 -29
  24. package/lib/commonjs/prompt-input/PromptInput.js.map +1 -1
  25. package/lib/commonjs/select/index.web.js +20 -22
  26. package/lib/commonjs/select/index.web.js.map +1 -1
  27. package/lib/commonjs/styles/z-index.js +33 -34
  28. package/lib/commonjs/styles/z-index.js.map +1 -1
  29. package/lib/commonjs/surfaces/SurfaceHost.js +3 -6
  30. package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
  31. package/lib/commonjs/toast/ToastHost.js +9 -5
  32. package/lib/commonjs/toast/ToastHost.js.map +1 -1
  33. package/lib/commonjs/tooltip/index.js +9 -6
  34. package/lib/commonjs/tooltip/index.js.map +1 -1
  35. package/lib/commonjs/tooltip/index.web.js +8 -2
  36. package/lib/commonjs/tooltip/index.web.js.map +1 -1
  37. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
  38. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  39. package/lib/module/avatar-group/AvatarGroup.web.js +21 -20
  40. package/lib/module/avatar-group/AvatarGroup.web.js.map +1 -1
  41. package/lib/module/bottom-sheet/index.web.js +11 -6
  42. package/lib/module/bottom-sheet/index.web.js.map +1 -1
  43. package/lib/module/context-menu/index.web.js +21 -23
  44. package/lib/module/context-menu/index.web.js.map +1 -1
  45. package/lib/module/dialog/Dialog.js +67 -71
  46. package/lib/module/dialog/Dialog.js.map +1 -1
  47. package/lib/module/dialog/Dialog.web.js +50 -52
  48. package/lib/module/dialog/Dialog.web.js.map +1 -1
  49. package/lib/module/fonts/font-data.web.js +18 -0
  50. package/lib/module/fonts/font-data.web.js.map +1 -0
  51. package/lib/module/menu/index.web.js +16 -12
  52. package/lib/module/menu/index.web.js.map +1 -1
  53. package/lib/module/overlay/index.js +99 -8
  54. package/lib/module/overlay/index.js.map +1 -1
  55. package/lib/module/overlay/stack.js +161 -0
  56. package/lib/module/overlay/stack.js.map +1 -0
  57. package/lib/module/overlay/use-overlay-layer.js +33 -0
  58. package/lib/module/overlay/use-overlay-layer.js.map +1 -0
  59. package/lib/module/popover/index.web.js +32 -34
  60. package/lib/module/popover/index.web.js.map +1 -1
  61. package/lib/module/prompt-input/PromptInput.js +33 -29
  62. package/lib/module/prompt-input/PromptInput.js.map +1 -1
  63. package/lib/module/select/index.web.js +21 -23
  64. package/lib/module/select/index.web.js.map +1 -1
  65. package/lib/module/styles/z-index.js +32 -30
  66. package/lib/module/styles/z-index.js.map +1 -1
  67. package/lib/module/surfaces/SurfaceHost.js +3 -6
  68. package/lib/module/surfaces/SurfaceHost.js.map +1 -1
  69. package/lib/module/toast/ToastHost.js +10 -6
  70. package/lib/module/toast/ToastHost.js.map +1 -1
  71. package/lib/module/tooltip/index.js +9 -6
  72. package/lib/module/tooltip/index.js.map +1 -1
  73. package/lib/module/tooltip/index.web.js +8 -2
  74. package/lib/module/tooltip/index.web.js.map +1 -1
  75. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
  76. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  77. package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/context-menu/index.web.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/dialog/types.d.ts +0 -10
  83. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/fonts/font-data.web.d.ts +5 -0
  85. package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/overlay/index.d.ts +31 -6
  88. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/overlay/stack.d.ts +122 -0
  90. package/lib/typescript/commonjs/overlay/stack.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts +7 -0
  92. package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/popover/index.web.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/prompt-input/PromptInput.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/styles/z-index.d.ts +42 -29
  97. package/lib/typescript/commonjs/styles/z-index.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/toast/ToastHost.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/tooltip/index.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  102. package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts.map +1 -1
  103. package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -1
  104. package/lib/typescript/module/context-menu/index.web.d.ts.map +1 -1
  105. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  106. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  107. package/lib/typescript/module/dialog/types.d.ts +0 -10
  108. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  109. package/lib/typescript/module/fonts/font-data.web.d.ts +5 -0
  110. package/lib/typescript/module/fonts/font-data.web.d.ts.map +1 -0
  111. package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
  112. package/lib/typescript/module/overlay/index.d.ts +31 -6
  113. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  114. package/lib/typescript/module/overlay/stack.d.ts +122 -0
  115. package/lib/typescript/module/overlay/stack.d.ts.map +1 -0
  116. package/lib/typescript/module/overlay/use-overlay-layer.d.ts +7 -0
  117. package/lib/typescript/module/overlay/use-overlay-layer.d.ts.map +1 -0
  118. package/lib/typescript/module/popover/index.web.d.ts.map +1 -1
  119. package/lib/typescript/module/prompt-input/PromptInput.d.ts.map +1 -1
  120. package/lib/typescript/module/select/index.web.d.ts.map +1 -1
  121. package/lib/typescript/module/styles/z-index.d.ts +42 -29
  122. package/lib/typescript/module/styles/z-index.d.ts.map +1 -1
  123. package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
  124. package/lib/typescript/module/toast/ToastHost.d.ts.map +1 -1
  125. package/lib/typescript/module/tooltip/index.d.ts.map +1 -1
  126. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  127. package/package.json +2 -1
  128. package/src/__tests__/ToastHostWebFork.test.ts +11 -3
  129. package/src/__tests__/overlay-stack-order.test.tsx +270 -0
  130. package/src/avatar-group/AvatarGroup.web.tsx +35 -31
  131. package/src/bottom-sheet/index.web.tsx +10 -5
  132. package/src/context-menu/index.web.tsx +33 -30
  133. package/src/dialog/Dialog.tsx +9 -18
  134. package/src/dialog/Dialog.web.tsx +62 -60
  135. package/src/dialog/types.ts +0 -10
  136. package/src/fonts/font-data.web.ts +15 -0
  137. package/src/menu/index.web.tsx +33 -22
  138. package/src/overlay/OverlayStacking.stories.tsx +207 -0
  139. package/src/overlay/index.tsx +114 -9
  140. package/src/overlay/stack.ts +166 -0
  141. package/src/overlay/use-overlay-layer.ts +39 -0
  142. package/src/popover/index.web.tsx +47 -44
  143. package/src/prompt-input/PromptInput.tsx +36 -29
  144. package/src/select/index.web.tsx +35 -32
  145. package/src/styles/z-index.ts +32 -34
  146. package/src/surfaces/SurfaceHost.tsx +3 -10
  147. package/src/toast/ToastHost.tsx +9 -6
  148. package/src/tooltip/index.tsx +14 -7
  149. package/src/tooltip/index.web.tsx +8 -2
  150. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +4 -4
@@ -1,49 +1,62 @@
1
1
  /**
2
- * Shared stacking scale for Bloom primitives.
2
+ * Stacking scale for stacking WITHIN a single context.
3
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.
4
+ * ## What belongs here, and what does not
5
+ *
6
+ * These values order elements against their own siblings — a badge over its
7
+ * avatar, a floating control over the panel it sits on, a sheet's drag handle
8
+ * over the sheet body. That is a local decision with a local answer, so a
9
+ * constant is the right tool.
10
+ *
11
+ * **Which OVERLAY SURFACE is on top of which is NOT on this scale**, and must
12
+ * never be added back to it. That question is answered by open order — the
13
+ * surface the user opened last wins — and is owned by `src/overlay/stack.ts`.
14
+ *
15
+ * This file used to carry rungs for that too (`dropdown` 40/41, `overlay`
16
+ * 50/60, `tooltip` 70, `toast` 80, `fullscreen` 90/91), plus
17
+ * `createOverlayZIndex` / `createDropdownZIndex` / `Z_INDEX_LAYER_STEP` to
18
+ * offset them. Because a rung is fixed per COMPONENT KIND, it decided stacking
19
+ * by what a surface WAS rather than by when it opened, so some pairings were
20
+ * permanently inverted: a confirm dialog (50/60) opened from inside an open
21
+ * bottom sheet (999999) rendered underneath it and could not be pressed or
22
+ * dismissed, and a menu (40/41) opened from inside a dialog landed behind that
23
+ * dialog. Those rungs and helpers are gone; reintroducing one reintroduces the
24
+ * bug.
7
25
  */
8
26
  export declare const Z_INDEX: {
27
+ /** Default resting level. */
9
28
  readonly base: 0;
29
+ /** One step above an immediate sibling — a badge over its avatar. */
10
30
  readonly raised: 1;
31
+ /** A control floating over the content it belongs to. */
11
32
  readonly floating: 10;
12
- readonly sticky: 20;
33
+ /** A dropdown's INLINE trigger wrapper, still in the app's document flow. */
13
34
  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;
35
+ /** A bottom sheet's drag handle, over the sheet's own body. */
21
36
  readonly sheetHandle: 100;
37
+ /**
38
+ * The document-level portal container itself (`#bloom-portal-root`). Overlay
39
+ * surfaces render INSIDE it and order among themselves by open order — none
40
+ * of them may claim this value, which is what made the sheet unbeatable.
41
+ */
22
42
  readonly portalRoot: 999999;
23
43
  };
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
44
  export declare const zIndex: {
45
+ /** Default resting level. */
34
46
  readonly base: 0;
47
+ /** One step above an immediate sibling — a badge over its avatar. */
35
48
  readonly raised: 1;
49
+ /** A control floating over the content it belongs to. */
36
50
  readonly floating: 10;
37
- readonly sticky: 20;
51
+ /** A dropdown's INLINE trigger wrapper, still in the app's document flow. */
38
52
  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;
53
+ /** A bottom sheet's drag handle, over the sheet's own body. */
46
54
  readonly sheetHandle: 100;
55
+ /**
56
+ * The document-level portal container itself (`#bloom-portal-root`). Overlay
57
+ * surfaces render INSIDE it and order among themselves by open order — none
58
+ * of them may claim this value, which is what made the sheet unbeatable.
59
+ */
47
60
  readonly portalRoot: 999999;
48
61
  };
49
62
  //# sourceMappingURL=z-index.d.ts.map
@@ -1 +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
+ {"version":3,"file":"z-index.d.ts","sourceRoot":"","sources":["../../../../src/styles/z-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,OAAO;IAClB,6BAA6B;;IAE7B,qEAAqE;;IAErE,yDAAyD;;IAEzD,6EAA6E;;IAE7E,+DAA+D;;IAE/D;;;;OAIG;;CAEK,CAAC;AAEX,eAAO,MAAM,MAAM;IAlBjB,6BAA6B;;IAE7B,qEAAqE;;IAErE,yDAAyD;;IAEzD,6EAA6E;;IAE7E,+DAA+D;;IAE/D;;;;OAIG;;CAIwB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAgCxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBAqGhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
1
+ {"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAgCxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBA8FhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ToastHost.d.ts","sourceRoot":"","sources":["../../../../src/toast/ToastHost.tsx"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,qBAAqB,EAAE,EAAE,cAAc,2CAwB5E;yBAxBe,SAAS"}
1
+ {"version":3,"file":"ToastHost.d.ts","sourceRoot":"","sources":["../../../../src/toast/ToastHost.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,qBAAqB,EAAE,EAAE,cAAc,2CAwB5E;yBAxBe,SAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tooltip/index.tsx"],"names":[],"mappings":"AAoEA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CAEA;yBANe,0BAA0B;;;AAS1C,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,QAAmB,EACnB,OAAO,EAAE,cAAc,EACvB,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,2CAsCA;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAuBzE;AAED,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf,kDAoBA;AAgND,eAAO,MAAM,iBAAiB;;6CAAmC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tooltip/index.tsx"],"names":[],"mappings":"AAqEA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,2CAEA;yBANe,0BAA0B;;;AAS1C,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,QAAmB,EACnB,OAAO,EAAE,cAAc,EACvB,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,2CAsCA;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAuBzE;AAED,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf,kDA0BA;AAgND,eAAO,MAAM,iBAAiB;;6CAAmC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAC;AAoElG,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AA87BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAC;AAmElG,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AA87BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AA0K9D,eAAe,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.72.2",
3
+ "version": "0.73.0",
4
+ "packageManager": "bun@1.3.14",
4
5
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
6
  "main": "lib/commonjs/index.js",
6
7
  "module": "lib/module/index.js",
@@ -52,9 +52,17 @@ describe('ToastHost platform split', () => {
52
52
  expect(host).not.toMatch(/'fixed' as/);
53
53
  });
54
54
 
55
- it('web/default host layers toasts at Z_INDEX.toast', () => {
56
- const host = read('toast/ToastHost.tsx');
57
- expect(host).toMatch(/zIndex: Z_INDEX\.toast/);
55
+ it('web/default host pins toasts above the whole overlay stack', () => {
56
+ const host = code('toast/ToastHost.tsx');
57
+ // A toast is a notification, not a modal surface: it must stay visible over
58
+ // whatever is open, including a surface opened AFTER it. So it opts out of
59
+ // the open-order stack via `OverlayRoot`'s `zIndex` prop rather than taking
60
+ // a rank (see `src/overlay/stack.ts`).
61
+ expect(host).toMatch(/<OverlayRoot zIndex=\{TOAST_LAYER_Z\}/);
62
+ // And it must not ALSO carry a hand-picked depth in its stylesheet: a fixed
63
+ // number there is exactly what left toasts stranded behind an open bottom
64
+ // sheet, and it would fight the prop above.
65
+ expect(host).not.toMatch(/zIndex: Z_INDEX\./);
58
66
  });
59
67
 
60
68
  it('native host imports neither react-native-screens nor the portal (D6)', () => {
@@ -0,0 +1,270 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ /**
6
+ * The overlay stacking rule: a surface opened LATER paints above one opened
7
+ * earlier.
8
+ *
9
+ * Jest cannot see the bug this guards — the losing surface renders perfectly
10
+ * and its styles are all valid; it is simply painted underneath. So these tests
11
+ * pin the two halves Jest CAN decide:
12
+ *
13
+ * 1. the authority in `src/overlay/stack.ts` hands out ascending depths, and
14
+ * keeps doing so across the interleavings that actually occur;
15
+ * 2. every overlay surface in `src/` gets its depth FROM that authority
16
+ * rather than from a constant of its own — the property whose absence
17
+ * caused the bug in the first place.
18
+ *
19
+ * The hit test that proves the user-visible outcome lives in
20
+ * `scripts/verify-overlay-stacking.mjs`, driven against a real browser.
21
+ */
22
+ import { readFileSync } from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ import { createElement } from 'react';
26
+ import { act } from 'react';
27
+ import { createRoot, type Root } from 'react-dom/client';
28
+
29
+ // The point of the DOM half is what react-native-web actually emits, so it runs
30
+ // against the REAL RNW — the repo-wide `react-native` mock renders no host
31
+ // elements and would make every depth assertion below vacuous.
32
+ jest.mock('react-native', () => jest.requireActual('react-native-web'));
33
+
34
+ import { OverlayRoot } from '../overlay';
35
+ import {
36
+ acquireOverlayRank,
37
+ layerForRank,
38
+ OVERLAY_STACK_BASE,
39
+ OVERLAY_STACK_MAX_RANK,
40
+ registerOverlayRank,
41
+ releaseOverlayRank,
42
+ resetOverlayStack,
43
+ TOAST_LAYER_Z,
44
+ } from '../overlay/stack';
45
+
46
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
47
+
48
+ beforeEach(() => {
49
+ resetOverlayStack();
50
+ });
51
+
52
+ describe('the stacking authority', () => {
53
+ it('gives a later opener a higher depth than an earlier one', () => {
54
+ const first = layerForRank(acquireOverlayRank());
55
+ const second = layerForRank(acquireOverlayRank());
56
+ const third = layerForRank(acquireOverlayRank());
57
+
58
+ expect(second.root).toBeGreaterThan(first.root);
59
+ expect(third.root).toBeGreaterThan(second.root);
60
+ });
61
+
62
+ it('puts a surface above its OWN backdrop without reaching the next surface', () => {
63
+ const lower = layerForRank(acquireOverlayRank());
64
+ const upper = layerForRank(acquireOverlayRank());
65
+
66
+ expect(lower.surface).toBeGreaterThan(lower.backdrop);
67
+ // The band is what keeps a surface's own parts from colliding with the
68
+ // floor of the surface above it.
69
+ expect(lower.surface).toBeLessThan(upper.backdrop);
70
+ });
71
+
72
+ it('keeps ordering when surfaces close out of order', () => {
73
+ // A opens, B opens, A closes, C opens. C is newest and must outrank B.
74
+ const a = acquireOverlayRank();
75
+ registerOverlayRank(a);
76
+ const b = acquireOverlayRank();
77
+ registerOverlayRank(b);
78
+
79
+ releaseOverlayRank(a);
80
+
81
+ const c = acquireOverlayRank();
82
+ registerOverlayRank(c);
83
+
84
+ expect(layerForRank(c).root).toBeGreaterThan(layerForRank(b).root);
85
+ });
86
+
87
+ it('starts over once every surface has closed, so depths stay small', () => {
88
+ const a = acquireOverlayRank();
89
+ registerOverlayRank(a);
90
+ releaseOverlayRank(a);
91
+
92
+ expect(layerForRank(acquireOverlayRank()).root).toBe(layerForRank(a).root);
93
+ });
94
+
95
+ it('cannot lose a rank handed out but not yet registered', () => {
96
+ // The one interleaving the reset could otherwise break: a surface takes a
97
+ // rank during render, and before its effect registers it the last open
98
+ // surface unmounts and resets the counter. The newcomer must still land
99
+ // ABOVE the one that is on its way up.
100
+ const open = acquireOverlayRank();
101
+ registerOverlayRank(open);
102
+
103
+ const rendering = acquireOverlayRank(); // taken, effect not run yet
104
+ releaseOverlayRank(open); // live set empties -> reset
105
+ registerOverlayRank(rendering); // effect finally runs
106
+
107
+ const next = acquireOverlayRank();
108
+ expect(next).toBeGreaterThan(rendering);
109
+ expect(layerForRank(next).root).toBeGreaterThan(layerForRank(rendering).root);
110
+ });
111
+
112
+ it('saturates instead of climbing into the toast layer', () => {
113
+ const runaway = layerForRank(OVERLAY_STACK_MAX_RANK + 5_000);
114
+ expect(runaway.surface).toBeLessThan(TOAST_LAYER_Z);
115
+ expect(runaway.root).toBe(layerForRank(OVERLAY_STACK_MAX_RANK).root);
116
+ });
117
+
118
+ it('keeps every rank below the toast layer', () => {
119
+ // Toasts are notifications, not modal surfaces: they stay visible over
120
+ // whatever is open, including a surface opened after them.
121
+ expect(layerForRank(1).root).toBeGreaterThanOrEqual(OVERLAY_STACK_BASE);
122
+ expect(layerForRank(OVERLAY_STACK_MAX_RANK).surface).toBeLessThan(TOAST_LAYER_Z);
123
+ });
124
+ });
125
+
126
+ describe('OverlayRoot', () => {
127
+ function render(node: ReturnType<typeof createElement>): {
128
+ root: Root;
129
+ container: HTMLElement;
130
+ } {
131
+ const container = document.createElement('div');
132
+ document.body.appendChild(container);
133
+ const root = createRoot(container);
134
+ act(() => {
135
+ root.render(node);
136
+ });
137
+ return { root, container };
138
+ }
139
+
140
+ const depthOf = (container: HTMLElement, testID: string) => {
141
+ const el = container.querySelector<HTMLElement>(`[data-testid="${testID}"]`);
142
+ if (!el) throw new Error(`no element with testID ${testID}`);
143
+ return Number(getComputedStyle(el).zIndex);
144
+ };
145
+
146
+ it('paints a surface mounted later above one mounted earlier', () => {
147
+ const first = render(createElement(OverlayRoot, { testID: 'first', children: null }));
148
+ const second = render(createElement(OverlayRoot, { testID: 'second', children: null }));
149
+
150
+ const firstZ = depthOf(first.container, 'first');
151
+ const secondZ = depthOf(second.container, 'second');
152
+
153
+ // Vacuity floor: both must have emitted a real depth, or "second > first"
154
+ // would be comparing two NaNs into a silent pass.
155
+ expect(Number.isFinite(firstZ)).toBe(true);
156
+ expect(Number.isFinite(secondZ)).toBe(true);
157
+ expect(secondZ).toBeGreaterThan(firstZ);
158
+
159
+ act(() => {
160
+ first.root.unmount();
161
+ second.root.unmount();
162
+ });
163
+ first.container.remove();
164
+ second.container.remove();
165
+ });
166
+
167
+ it('pins to the given depth, and takes no rank, when told to', () => {
168
+ const pinned = render(
169
+ createElement(OverlayRoot, { testID: 'pinned', zIndex: TOAST_LAYER_Z, children: null }),
170
+ );
171
+ expect(depthOf(pinned.container, 'pinned')).toBe(TOAST_LAYER_Z);
172
+
173
+ // Having opted out, it must not have consumed a rank either — the next
174
+ // surface to open is still the FIRST one in the stack.
175
+ const ranked = render(createElement(OverlayRoot, { testID: 'ranked', children: null }));
176
+ expect(depthOf(ranked.container, 'ranked')).toBe(layerForRank(1).root);
177
+
178
+ act(() => {
179
+ pinned.root.unmount();
180
+ ranked.root.unmount();
181
+ });
182
+ pinned.container.remove();
183
+ ranked.container.remove();
184
+ });
185
+ });
186
+
187
+ describe('no surface picks its own depth', () => {
188
+ const SRC = path.join(__dirname, '..');
189
+ const read = (rel: string) => readFileSync(path.join(SRC, rel), 'utf8');
190
+
191
+ /**
192
+ * Every file in `src/` that presents an overlay surface. Enumerated
193
+ * explicitly rather than globbed: a glob that silently stops matching is the
194
+ * shape of check that passes forever while covering nothing.
195
+ */
196
+ const SURFACES = [
197
+ 'dialog/Dialog.tsx',
198
+ 'dialog/Dialog.web.tsx',
199
+ 'bottom-sheet/index.web.tsx',
200
+ 'menu/index.web.tsx',
201
+ 'select/index.web.tsx',
202
+ 'context-menu/index.web.tsx',
203
+ 'popover/index.web.tsx',
204
+ 'tooltip/index.tsx',
205
+ 'zoomable-image-gallery/ZoomableImageGallery.tsx',
206
+ 'prompt-input/PromptInput.tsx',
207
+ 'avatar-group/AvatarGroup.web.tsx',
208
+ 'toast/ToastHost.tsx',
209
+ ];
210
+
211
+ /** Strips comments, so prose ABOUT the old constants never trips the scan. */
212
+ const code = (rel: string) =>
213
+ read(rel)
214
+ .replace(/\/\*[\s\S]*?\*\//g, '')
215
+ .replace(/^\s*\/\/.*$/gm, '')
216
+ .replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, '');
217
+
218
+ it.each(SURFACES)('%s claims no overlay-ordering constant', (rel) => {
219
+ const body = code(rel);
220
+
221
+ // The rungs that used to decide which SURFACE was on top. Their absence is
222
+ // the property that makes open order the only authority.
223
+ expect(body).not.toMatch(
224
+ /Z_INDEX\.(overlayBackdrop|overlaySurface|dropdownSurface|tooltip|toast|fullscreen|fullscreenControl|portalRoot)\b/,
225
+ );
226
+ expect(body).not.toMatch(/createOverlayZIndex|createDropdownZIndex|Z_INDEX_LAYER_STEP/);
227
+ });
228
+
229
+ it('the scale no longer offers an overlay-ordering rung to reach for', () => {
230
+ const scale = code('styles/z-index.ts');
231
+ for (const gone of [
232
+ 'overlayBackdrop',
233
+ 'overlaySurface',
234
+ 'dropdownSurface',
235
+ 'tooltip',
236
+ 'toast',
237
+ 'fullscreen',
238
+ 'fullscreenControl',
239
+ 'createOverlayZIndex',
240
+ 'createDropdownZIndex',
241
+ 'Z_INDEX_LAYER_STEP',
242
+ ]) {
243
+ expect(scale).not.toContain(gone);
244
+ }
245
+ // Vacuity floor: the module must still export the within-context scale, or
246
+ // the loop above would pass against an empty file.
247
+ expect(scale).toMatch(/export const Z_INDEX = \{/);
248
+ expect(scale).toMatch(/portalRoot: 999999/);
249
+ });
250
+
251
+ it('the WEB tooltip is deliberately NOT in this list', () => {
252
+ // It is the one tooltip that is not portaled: the bubble is absolutely
253
+ // positioned against its own trigger and renders inline, so it cannot be
254
+ // lifted over another surface and has no rank to take. The NATIVE tooltip
255
+ // does portal, and IS in the list above. If this ever starts portaling,
256
+ // move it into `SURFACES`.
257
+ expect(code('tooltip/index.web.tsx')).not.toMatch(/from '\.\.\/portal/);
258
+ expect(code('tooltip/index.tsx')).toMatch(/from '\.\.\/portal/);
259
+ });
260
+
261
+ it('every surface routes its depth through OverlayRoot', () => {
262
+ // `Dialog.web`'s side placement renders `SheetSurface`, which mounts its
263
+ // own `OverlayRoot`; the centred branch mounts one directly. Both files are
264
+ // covered by the membership check below.
265
+ const missing = SURFACES.filter((rel) => !code(rel).includes('OverlayRoot'));
266
+ expect(missing).toEqual([]);
267
+ // Vacuity floor on the list itself.
268
+ expect(SURFACES.length).toBeGreaterThanOrEqual(12);
269
+ });
270
+ });
@@ -1,8 +1,8 @@
1
1
  import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
2
2
  import { Pressable, StyleSheet, View } from 'react-native';
3
3
 
4
+ import { OverlayRoot } from '../overlay';
4
5
  import { Portal } from '../portal';
5
- import { Z_INDEX } from '../styles/z-index';
6
6
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
7
7
  import { UserHoverCard } from '../user-hover-card';
8
8
  import {
@@ -97,35 +97,40 @@ const AvatarGroupWebComponent: React.FC<AvatarGroupProps> = (props) => {
97
97
  <AvatarGroupBase {...props} hoverHandlers={hoverHandlers} />
98
98
  {hoverCard && hover && (
99
99
  <Portal>
100
- {/*
101
- A Pressable hosts the hover-bridge: `onHoverIn`/`onHoverOut` are
102
- typed in React Native (and map to mouseenter/mouseleave on
103
- react-native-web), so the card stays open while the cursor is over
104
- it without any web-only DOM-prop type suppression.
105
- */}
106
- <Pressable
107
- onHoverIn={cancelClose}
108
- onHoverOut={scheduleClose}
109
- accessibilityRole="none"
110
- pointerEvents="auto"
111
- style={[
112
- webStyles.floating,
113
- { top: hover.top, left: hover.left, width: CARD_WIDTH },
114
- ]}
115
- >
116
- <UserHoverCard
117
- avatar={hover.item.uri ?? undefined}
118
- displayName={getItemName(hover.item)}
119
- username={hover.item.username}
120
- onPressProfile={
121
- onPressItem
122
- ? () => onPressItem(hover.item, hover.index)
123
- : undefined
124
- }
125
- action={renderItemAction?.(hover.item, hover.index)}
126
- style={webStyles.card}
127
- />
128
- </Pressable>
100
+ {/* The hover card stacks by when it opened, like every other Bloom
101
+ overlay (`src/overlay/stack.ts`). `box-none` keeps the area
102
+ around it click-through. */}
103
+ <OverlayRoot>
104
+ {/*
105
+ A Pressable hosts the hover-bridge: `onHoverIn`/`onHoverOut` are
106
+ typed in React Native (and map to mouseenter/mouseleave on
107
+ react-native-web), so the card stays open while the cursor is over
108
+ it without any web-only DOM-prop type suppression.
109
+ */}
110
+ <Pressable
111
+ onHoverIn={cancelClose}
112
+ onHoverOut={scheduleClose}
113
+ accessibilityRole="none"
114
+ pointerEvents="auto"
115
+ style={[
116
+ webStyles.floating,
117
+ { top: hover.top, left: hover.left, width: CARD_WIDTH },
118
+ ]}
119
+ >
120
+ <UserHoverCard
121
+ avatar={hover.item.uri ?? undefined}
122
+ displayName={getItemName(hover.item)}
123
+ username={hover.item.username}
124
+ onPressProfile={
125
+ onPressItem
126
+ ? () => onPressItem(hover.item, hover.index)
127
+ : undefined
128
+ }
129
+ action={renderItemAction?.(hover.item, hover.index)}
130
+ style={webStyles.card}
131
+ />
132
+ </Pressable>
133
+ </OverlayRoot>
129
134
  </Portal>
130
135
  )}
131
136
  </>
@@ -135,7 +140,6 @@ const AvatarGroupWebComponent: React.FC<AvatarGroupProps> = (props) => {
135
140
  const webStyles = StyleSheet.create({
136
141
  floating: {
137
142
  position: WEB_POSITION_FIXED,
138
- zIndex: Z_INDEX.tooltip,
139
143
  },
140
144
  card: {
141
145
  width: CARD_WIDTH,
@@ -4,7 +4,6 @@ import { StyleSheet } from 'react-native';
4
4
  import { GestureHandlerRootView } from 'react-native-gesture-handler';
5
5
  import { OverlayRoot } from '../overlay';
6
6
  import { Portal } from '../portal/index.web';
7
- import { Z_INDEX } from '../styles/z-index';
8
7
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
9
8
  import {
10
9
  BottomSheetBase,
@@ -54,16 +53,22 @@ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef
54
53
  BottomSheet.displayName = 'BottomSheet';
55
54
 
56
55
  const webStyles = StyleSheet.create({
56
+ // Fixed, full-viewport box the sheet's `StyleSheet.absoluteFill` body
57
+ // anchors to. The pointer-events opt-in and the STACKING both live in
58
+ // `OverlayRoot`.
59
+ //
60
+ // This used to also pin `zIndex: Z_INDEX.portalRoot` (999999) here. Inside
61
+ // the portal root that number does not mean "the portal layer" — it means
62
+ // "above every other portaled surface", which is how a confirm dialog
63
+ // opened from inside an open sheet ended up rendering underneath it,
64
+ // unreachable, no matter which one opened last. Stacking is the overlay
65
+ // stack's job now (`src/overlay/stack.ts`); do not reintroduce a constant.
57
66
  rootView: {
58
- // Fixed, full-viewport box the sheet's `StyleSheet.absoluteFill` body
59
- // anchors to. The pointer-events opt-in lives in `OverlayRoot`; this
60
- // only adds the stacking context.
61
67
  position: WEB_POSITION_FIXED,
62
68
  top: 0,
63
69
  left: 0,
64
70
  right: 0,
65
71
  bottom: 0,
66
- zIndex: Z_INDEX.portalRoot,
67
72
  },
68
73
  });
69
74
 
@@ -18,10 +18,9 @@ import { Pressable, StyleSheet, View } from 'react-native';
18
18
 
19
19
  import { useTheme } from '../theme/use-theme';
20
20
  import { Text } from '../typography';
21
- import { Backdrop } from '../overlay';
21
+ import { Backdrop, OverlayRoot } from '../overlay';
22
22
  import { Portal } from '../portal/index.web';
23
23
  import { useInteractionState } from '../hooks/useInteractionState';
24
- import { createOverlayZIndex } from '../styles/z-index';
25
24
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
26
25
  import {
27
26
  resolveDropdownPlacement,
@@ -40,7 +39,6 @@ import type {
40
39
  ItemContextValue,
41
40
  } from './types';
42
41
 
43
- const contextMenuZIndex = createOverlayZIndex();
44
42
  const VIEWPORT_GUTTER = 8;
45
43
 
46
44
  // ---------------------------------------------------------------------------
@@ -211,31 +209,38 @@ export function ContextMenuContent({ children, style }: ContextMenuContentProps)
211
209
 
212
210
  return (
213
211
  <Portal>
214
- <Backdrop
215
- style={styles.backdrop}
216
- onPress={close}
217
- accessibilityLabel="Close context menu"
218
- />
219
- <View
220
- ref={attachMenu}
221
- style={[
222
- styles.dropdown,
223
- {
224
- // The raw click point is the pre-measurement anchor; the layout
225
- // effect above replaces it before the browser paints.
226
- top: placement?.top ?? position.y,
227
- left: placement?.left ?? position.x,
228
- backgroundColor: theme.isDark
229
- ? theme.colors.backgroundSecondary
230
- : theme.colors.background,
231
- borderColor: theme.colors.borderLight,
232
- ...bloomShadowStyle('m'),
233
- },
234
- style,
235
- ]}
236
- >
237
- {children}
238
- </View>
212
+ {/* `OverlayRoot` takes this surface's place in the open-order overlay
213
+ stack, so it paints above anything opened before it (see
214
+ `src/overlay/stack.ts`). It is `box-none`, so the area outside the
215
+ panel stays click-through and the backdrop below still takes its own
216
+ presses. */}
217
+ <OverlayRoot>
218
+ <Backdrop
219
+ style={styles.backdrop}
220
+ onPress={close}
221
+ accessibilityLabel="Close context menu"
222
+ />
223
+ <View
224
+ ref={attachMenu}
225
+ style={[
226
+ styles.dropdown,
227
+ {
228
+ // The raw click point is the pre-measurement anchor; the layout
229
+ // effect above replaces it before the browser paints.
230
+ top: placement?.top ?? position.y,
231
+ left: placement?.left ?? position.x,
232
+ backgroundColor: theme.isDark
233
+ ? theme.colors.backgroundSecondary
234
+ : theme.colors.background,
235
+ borderColor: theme.colors.borderLight,
236
+ ...bloomShadowStyle('m'),
237
+ },
238
+ style,
239
+ ]}
240
+ >
241
+ {children}
242
+ </View>
243
+ </OverlayRoot>
239
244
  </Portal>
240
245
  );
241
246
  }
@@ -374,13 +379,11 @@ const styles = StyleSheet.create({
374
379
  left: 0,
375
380
  right: 0,
376
381
  bottom: 0,
377
- zIndex: contextMenuZIndex.backdrop,
378
382
  // Opt back in from the Portal root's `pointer-events: none`.
379
383
  pointerEvents: 'auto',
380
384
  },
381
385
  dropdown: {
382
386
  position: WEB_POSITION_FIXED,
383
- zIndex: contextMenuZIndex.surface,
384
387
  borderRadius: 8,
385
388
  padding: 4,
386
389
  borderWidth: 1,