@oxyhq/bloom 0.72.1 → 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 (151) 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 +186 -39
  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 +152 -40
  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 +47 -10
  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 +47 -10
  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__/backdrop-nesting.test.tsx +95 -0
  130. package/src/__tests__/overlay-stack-order.test.tsx +270 -0
  131. package/src/avatar-group/AvatarGroup.web.tsx +35 -31
  132. package/src/bottom-sheet/index.web.tsx +10 -5
  133. package/src/context-menu/index.web.tsx +33 -30
  134. package/src/dialog/Dialog.tsx +9 -18
  135. package/src/dialog/Dialog.web.tsx +62 -60
  136. package/src/dialog/types.ts +0 -10
  137. package/src/fonts/font-data.web.ts +15 -0
  138. package/src/menu/index.web.tsx +33 -22
  139. package/src/overlay/OverlayStacking.stories.tsx +207 -0
  140. package/src/overlay/index.tsx +161 -40
  141. package/src/overlay/stack.ts +166 -0
  142. package/src/overlay/use-overlay-layer.ts +39 -0
  143. package/src/popover/index.web.tsx +47 -44
  144. package/src/prompt-input/PromptInput.tsx +36 -29
  145. package/src/select/index.web.tsx +35 -32
  146. package/src/styles/z-index.ts +32 -34
  147. package/src/surfaces/SurfaceHost.tsx +3 -10
  148. package/src/toast/ToastHost.tsx +9 -6
  149. package/src/tooltip/index.tsx +14 -7
  150. package/src/tooltip/index.web.tsx +8 -2
  151. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/popover/index.web.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAkB,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EAEnB,YAAY,EACZ,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAI7B;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,mBAAmB,CAoBvD;AASD,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,YAAY,2CAkD1D;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,mBAAmB,2CA+BtE;AAuCD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAiB,EACjB,SAA0B,EAC1B,MAAU,EACV,QAAQ,EACR,QAAc,EACd,WAAkB,EAClB,KAAK,EACL,MAAM,GACP,EAAE,mBAAmB,kDAmGrB"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/popover/index.web.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EAEnB,YAAY,EACZ,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAG7B;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,mBAAmB,CAoBvD;AASD,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,YAAY,2CAkD1D;AAED,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,mBAAmB,2CA+BtE;AAuCD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAiB,EACjB,SAA0B,EAC1B,MAAU,EACV,QAAQ,EACR,QAAc,EACd,WAAkB,EAClB,KAAK,EACL,MAAM,GACP,EAAE,mBAAmB,kDA0GrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../../src/prompt-input/PromptInput.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,wBAAgB,WAAW,CAAC,EAC1B,SAAiB,EACjB,SAAe,EACf,KAAK,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAgB,EAChB,YAAY,EACZ,WAAW,EACX,WAAW,EACX,MAAM,EACN,WAAW,EACX,WAAW,EAAE,qBAAqB,EAClC,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,wBAAgC,EAChC,UAAU,EACV,YAAY,EACZ,MAAM,GACP,EAAE,gBAAgB,2CA+NlB;yBArPe,WAAW"}
1
+ {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../../src/prompt-input/PromptInput.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,wBAAgB,WAAW,CAAC,EAC1B,SAAiB,EACjB,SAAe,EACf,KAAK,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAgB,EAChB,YAAY,EACZ,WAAW,EACX,WAAW,EACX,MAAM,EACN,WAAW,EACX,WAAW,EAAE,qBAAqB,EAClC,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,wBAAgC,EAChC,UAAU,EACV,YAAY,EACZ,MAAM,GACP,EAAE,gBAAgB,2CAqOlB;yBA3Pe,WAAW"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/select/index.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AAiBf,OAAO,EAA0C,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACxF,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,WAAW,EAEX,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAiChC,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,WAAW,2CAkB/E;AAMD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,kBAAkB,yTAyDpE;AAMD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EAAE,YAAY,EACtB,WAAW,EACX,KAAK,GACN,EAAE,gBAAgB,2CAkBlB;AAMD,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,eAAe,2CAGpD;AAMD,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAC/B,KAAK,EACL,UAAU,EACV,KAA0B,EAC1B,cAA0C,GAC3C,EAAE,kBAAkB,CAAC,CAAC,CAAC,kDAyFvB;AAMD,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,2CA6CjF;AAMD,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,mBAAmB,2CAMtE;AAMD,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,aAAyB,EAAE,EAAE,wBAAwB,2CAYhG;AAMD,wBAAgB,eAAe,4CAW9B"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/select/index.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AAgBf,OAAO,EAA0C,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACxF,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,WAAW,EAEX,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAgChC,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,WAAW,2CAkB/E;AAMD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,kBAAkB,yTAyDpE;AAMD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EAAE,YAAY,EACtB,WAAW,EACX,KAAK,GACN,EAAE,gBAAgB,2CAkBlB;AAMD,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,eAAe,2CAGpD;AAMD,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAC/B,KAAK,EACL,UAAU,EACV,KAA0B,EAC1B,cAA0C,GAC3C,EAAE,kBAAkB,CAAC,CAAC,CAAC,kDAgGvB;AAMD,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,2CA6CjF;AAMD,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,mBAAmB,2CAMtE;AAMD,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,aAAyB,EAAE,EAAE,wBAAwB,2CAYhG;AAMD,wBAAgB,eAAe,4CAW9B"}
@@ -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.1",
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,95 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ // The bug these tests exist for: `Backdrop`'s dismiss target carries
6
+ // `accessibilityRole="button"`, which react-native-web renders as a real
7
+ // `<button>`. While that target WRAPPED `children`, every control inside a
8
+ // Bloom surface became a nested `<button>` — invalid HTML, reported by React as
9
+ // a hydration error, and with activation behaviour the spec leaves undefined.
10
+ // It reproduced on any dialog with a button in it (Mention's post-options menu:
11
+ // "Dismiss Post options" containing "Insights").
12
+ //
13
+ // The fix makes the hit box a SIBLING behind `children`. These tests pin both
14
+ // halves of that: no nesting, and the dismiss press still works.
15
+
16
+ import { createElement } from 'react';
17
+ import { act } from 'react';
18
+ import { createRoot, type Root } from 'react-dom/client';
19
+
20
+ // The whole point is what react-native-web puts in the DOM, so this suite runs
21
+ // against the REAL RNW — the repo-wide `react-native` mock emits no host
22
+ // elements and would make every assertion below vacuous.
23
+ jest.mock('react-native', () => jest.requireActual('react-native-web'));
24
+
25
+ import { Pressable, Text } from 'react-native';
26
+
27
+ import { Backdrop } from '../overlay';
28
+
29
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
30
+
31
+ function render(node: ReturnType<typeof createElement>): { root: Root; container: HTMLElement } {
32
+ const container = document.createElement('div');
33
+ document.body.appendChild(container);
34
+ const root = createRoot(container);
35
+ act(() => {
36
+ root.render(node);
37
+ });
38
+ return { root, container };
39
+ }
40
+
41
+ describe('Backdrop DOM nesting (web)', () => {
42
+ it('does not make a pressable child a nested <button>', () => {
43
+ const { root, container } = render(
44
+ createElement(Backdrop, {
45
+ onPress: () => {},
46
+ accessibilityLabel: 'Dismiss Post options',
47
+ testID: 'backdrop',
48
+ children: createElement(
49
+ Pressable,
50
+ { accessibilityRole: 'button', accessibilityLabel: 'Insights', onPress: () => {} },
51
+ createElement(Text, null, 'Insights'),
52
+ ),
53
+ }),
54
+ );
55
+
56
+ // Vacuity floor: the child button must actually be in the DOM, otherwise
57
+ // "no nesting" would pass on an empty tree.
58
+ const inner = container.querySelector('[aria-label="Insights"]');
59
+ expect(inner?.tagName).toBe('BUTTON');
60
+ expect(container.querySelector('[aria-label="Dismiss Post options"]')?.tagName).toBe('BUTTON');
61
+
62
+ expect(container.querySelectorAll('button button')).toHaveLength(0);
63
+
64
+ act(() => root.unmount());
65
+ container.remove();
66
+ });
67
+
68
+ it('still dismisses when the backdrop itself is pressed', () => {
69
+ const onPress = jest.fn();
70
+ const { root, container } = render(
71
+ createElement(Backdrop, { onPress, accessibilityLabel: 'Dismiss dialog', children: null }),
72
+ );
73
+
74
+ const hitBox = container.querySelector<HTMLElement>('[aria-label="Dismiss dialog"]');
75
+ expect(hitBox).not.toBeNull();
76
+ act(() => {
77
+ hitBox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
78
+ });
79
+ expect(onPress).toHaveBeenCalledTimes(1);
80
+
81
+ act(() => root.unmount());
82
+ container.remove();
83
+ });
84
+
85
+ it('emits no dismiss affordance at all when inert', () => {
86
+ const { root, container } = render(
87
+ createElement(Backdrop, { disabled: true, onPress: () => {}, children: null }),
88
+ );
89
+
90
+ expect(container.querySelectorAll('button')).toHaveLength(0);
91
+
92
+ act(() => root.unmount());
93
+ container.remove();
94
+ });
95
+ });
@@ -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,