@oxyhq/bloom 0.52.0 → 0.53.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 (205) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -4
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
  4. package/lib/commonjs/design-tokens/shadows.js +4 -0
  5. package/lib/commonjs/design-tokens/shadows.js.map +1 -1
  6. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  7. package/lib/commonjs/dialog/Dialog.web.js +26 -10
  8. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  9. package/lib/commonjs/index.js +66 -66
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/index.web.js +90 -90
  12. package/lib/commonjs/index.web.js.map +1 -1
  13. package/lib/commonjs/loading/Loading.web.js.map +1 -1
  14. package/lib/commonjs/loading/SpinnerIcon.web.js +10 -6
  15. package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
  16. package/lib/commonjs/motion/index.js +1 -1
  17. package/lib/commonjs/motion/index.web.js +32 -0
  18. package/lib/commonjs/motion/index.web.js.map +1 -0
  19. package/lib/commonjs/motion/motion.js +10 -2
  20. package/lib/commonjs/motion/motion.js.map +1 -1
  21. package/lib/commonjs/motion/motion.web.js +137 -0
  22. package/lib/commonjs/motion/motion.web.js.map +1 -0
  23. package/lib/commonjs/progressive-blur/index.web.js.map +1 -1
  24. package/lib/commonjs/styles/web-view-style.js +33 -0
  25. package/lib/commonjs/styles/web-view-style.js.map +1 -1
  26. package/lib/commonjs/tab-bar/TabBarBase.js +18 -4
  27. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  28. package/lib/commonjs/tab-bar/index.js +5 -5
  29. package/lib/commonjs/tab-bar/index.js.map +1 -1
  30. package/lib/commonjs/tab-bar/index.web.js +5 -5
  31. package/lib/commonjs/tab-bar/index.web.js.map +1 -1
  32. package/lib/commonjs/tab-bar/shared.js +3 -3
  33. package/lib/commonjs/tab-bar/surface.web.js.map +1 -1
  34. package/lib/commonjs/tab-bar/use-footprint.js +56 -0
  35. package/lib/commonjs/tab-bar/use-footprint.js.map +1 -0
  36. package/lib/commonjs/toast/ToastRow.js +7 -3
  37. package/lib/commonjs/toast/ToastRow.js.map +1 -1
  38. package/lib/commonjs/toast/ToastSwipeHandler.js +29 -13
  39. package/lib/commonjs/toast/ToastSwipeHandler.js.map +1 -1
  40. package/lib/commonjs/toast/constants.js +29 -1
  41. package/lib/commonjs/toast/constants.js.map +1 -1
  42. package/lib/commonjs/toast/position-utils.js +9 -4
  43. package/lib/commonjs/toast/position-utils.js.map +1 -1
  44. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
  45. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  46. package/lib/module/bottom-sheet/BottomSheetBase.js +7 -4
  47. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  48. package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
  49. package/lib/module/design-tokens/shadows.js +4 -0
  50. package/lib/module/design-tokens/shadows.js.map +1 -1
  51. package/lib/module/dialog/Dialog.js.map +1 -1
  52. package/lib/module/dialog/Dialog.web.js +26 -10
  53. package/lib/module/dialog/Dialog.web.js.map +1 -1
  54. package/lib/module/index.js +1 -1
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/index.web.js +1 -1
  57. package/lib/module/index.web.js.map +1 -1
  58. package/lib/module/loading/Loading.web.js.map +1 -1
  59. package/lib/module/loading/SpinnerIcon.web.js +10 -6
  60. package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
  61. package/lib/module/motion/index.js +1 -1
  62. package/lib/module/motion/index.js.map +1 -1
  63. package/lib/module/motion/index.web.js +10 -0
  64. package/lib/module/motion/index.web.js.map +1 -0
  65. package/lib/module/motion/motion.js +10 -2
  66. package/lib/module/motion/motion.js.map +1 -1
  67. package/lib/module/motion/motion.web.js +133 -0
  68. package/lib/module/motion/motion.web.js.map +1 -0
  69. package/lib/module/progressive-blur/index.web.js.map +1 -1
  70. package/lib/module/styles/web-view-style.js +33 -0
  71. package/lib/module/styles/web-view-style.js.map +1 -1
  72. package/lib/module/tab-bar/TabBarBase.js +18 -4
  73. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  74. package/lib/module/tab-bar/index.js +1 -1
  75. package/lib/module/tab-bar/index.js.map +1 -1
  76. package/lib/module/tab-bar/index.web.js +1 -1
  77. package/lib/module/tab-bar/index.web.js.map +1 -1
  78. package/lib/module/tab-bar/shared.js +3 -3
  79. package/lib/module/tab-bar/surface.web.js.map +1 -1
  80. package/lib/module/tab-bar/use-footprint.js +52 -0
  81. package/lib/module/tab-bar/use-footprint.js.map +1 -0
  82. package/lib/module/toast/ToastRow.js +8 -4
  83. package/lib/module/toast/ToastRow.js.map +1 -1
  84. package/lib/module/toast/ToastSwipeHandler.js +29 -13
  85. package/lib/module/toast/ToastSwipeHandler.js.map +1 -1
  86. package/lib/module/toast/constants.js +28 -1
  87. package/lib/module/toast/constants.js.map +1 -1
  88. package/lib/module/toast/position-utils.js +9 -4
  89. package/lib/module/toast/position-utils.js.map +1 -1
  90. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
  91. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  92. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/design-tokens/shadows.d.ts +4 -0
  95. package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  98. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/motion/index.web.d.ts +9 -0
  102. package/lib/typescript/commonjs/motion/index.web.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/motion/motion.d.ts +14 -6
  104. package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/motion/motion.web.d.ts +79 -0
  106. package/lib/typescript/commonjs/motion/motion.web.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/progressive-blur/index.web.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/styles/web-view-style.d.ts +49 -0
  109. package/lib/typescript/commonjs/styles/web-view-style.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/tab-bar/index.d.ts +1 -1
  112. package/lib/typescript/commonjs/tab-bar/index.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/tab-bar/index.web.d.ts +1 -1
  114. package/lib/typescript/commonjs/tab-bar/index.web.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/tab-bar/shared.d.ts +3 -3
  116. package/lib/typescript/commonjs/tab-bar/surface.web.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +31 -0
  118. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/toast/ToastRow.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts +12 -6
  121. package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/toast/constants.d.ts +26 -8
  123. package/lib/typescript/commonjs/toast/constants.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/toast/position-utils.d.ts +8 -3
  125. package/lib/typescript/commonjs/toast/position-utils.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  127. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  128. package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  129. package/lib/typescript/module/design-tokens/shadows.d.ts +4 -0
  130. package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -1
  131. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  132. package/lib/typescript/module/index.web.d.ts +2 -2
  133. package/lib/typescript/module/index.web.d.ts.map +1 -1
  134. package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
  135. package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
  136. package/lib/typescript/module/motion/index.web.d.ts +9 -0
  137. package/lib/typescript/module/motion/index.web.d.ts.map +1 -0
  138. package/lib/typescript/module/motion/motion.d.ts +14 -6
  139. package/lib/typescript/module/motion/motion.d.ts.map +1 -1
  140. package/lib/typescript/module/motion/motion.web.d.ts +79 -0
  141. package/lib/typescript/module/motion/motion.web.d.ts.map +1 -0
  142. package/lib/typescript/module/progressive-blur/index.web.d.ts.map +1 -1
  143. package/lib/typescript/module/styles/web-view-style.d.ts +49 -0
  144. package/lib/typescript/module/styles/web-view-style.d.ts.map +1 -1
  145. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  146. package/lib/typescript/module/tab-bar/index.d.ts +1 -1
  147. package/lib/typescript/module/tab-bar/index.d.ts.map +1 -1
  148. package/lib/typescript/module/tab-bar/index.web.d.ts +1 -1
  149. package/lib/typescript/module/tab-bar/index.web.d.ts.map +1 -1
  150. package/lib/typescript/module/tab-bar/shared.d.ts +3 -3
  151. package/lib/typescript/module/tab-bar/surface.web.d.ts.map +1 -1
  152. package/lib/typescript/module/tab-bar/use-footprint.d.ts +31 -0
  153. package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -0
  154. package/lib/typescript/module/toast/ToastRow.d.ts.map +1 -1
  155. package/lib/typescript/module/toast/ToastSwipeHandler.d.ts +12 -6
  156. package/lib/typescript/module/toast/ToastSwipeHandler.d.ts.map +1 -1
  157. package/lib/typescript/module/toast/constants.d.ts +26 -8
  158. package/lib/typescript/module/toast/constants.d.ts.map +1 -1
  159. package/lib/typescript/module/toast/position-utils.d.ts +8 -3
  160. package/lib/typescript/module/toast/position-utils.d.ts.map +1 -1
  161. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  162. package/package.json +6 -1
  163. package/src/__tests__/Motion.test.tsx +31 -3
  164. package/src/__tests__/MotionWebFork.test.ts +140 -0
  165. package/src/__tests__/TabBarAnimatedShorthand.test.ts +135 -0
  166. package/src/__tests__/TabBarFootprint.test.tsx +172 -0
  167. package/src/__tests__/TabBarWebFork.test.ts +1 -1
  168. package/src/__tests__/ToastHostWebFork.test.ts +6 -2
  169. package/src/__tests__/Toaster.test.tsx +62 -1
  170. package/src/__tests__/toast-position-utils.test.ts +23 -7
  171. package/src/__tests__/web-css-style.test.ts +139 -0
  172. package/src/bottom-sheet/BottomSheetBase.tsx +11 -7
  173. package/src/connection-dots/ConnectionDots.web.tsx +4 -3
  174. package/src/design-tokens/shadows.ts +5 -1
  175. package/src/dialog/Dialog.tsx +1 -1
  176. package/src/dialog/Dialog.web.tsx +41 -26
  177. package/src/index.web.ts +2 -2
  178. package/src/loading/Loading.web.tsx +7 -5
  179. package/src/loading/SpinnerIcon.web.tsx +12 -9
  180. package/src/motion/index.web.ts +8 -0
  181. package/src/motion/motion.ts +14 -6
  182. package/src/motion/motion.web.ts +99 -0
  183. package/src/progressive-blur/index.web.tsx +4 -3
  184. package/src/styles/web-view-style.ts +50 -0
  185. package/src/tab-bar/TabBarBase.tsx +17 -12
  186. package/src/tab-bar/index.ts +1 -1
  187. package/src/tab-bar/index.web.tsx +1 -1
  188. package/src/tab-bar/shared.ts +3 -3
  189. package/src/tab-bar/surface.web.tsx +4 -3
  190. package/src/tab-bar/use-footprint.ts +50 -0
  191. package/src/toast/ToastRow.tsx +8 -3
  192. package/src/toast/ToastSwipeHandler.tsx +30 -12
  193. package/src/toast/constants.ts +28 -0
  194. package/src/toast/position-utils.ts +10 -5
  195. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +9 -8
  196. package/lib/commonjs/tab-bar/use-reserved-space.js +0 -50
  197. package/lib/commonjs/tab-bar/use-reserved-space.js.map +0 -1
  198. package/lib/module/tab-bar/use-reserved-space.js +0 -46
  199. package/lib/module/tab-bar/use-reserved-space.js.map +0 -1
  200. package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts +0 -25
  201. package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts.map +0 -1
  202. package/lib/typescript/module/tab-bar/use-reserved-space.d.ts +0 -25
  203. package/lib/typescript/module/tab-bar/use-reserved-space.d.ts.map +0 -1
  204. package/src/__tests__/TabBarReservedSpace.test.tsx +0 -150
  205. package/src/tab-bar/use-reserved-space.ts +0 -44
@@ -0,0 +1,50 @@
1
+ /**
2
+ * How much room the floating bar physically takes at the bottom of the window.
3
+ *
4
+ * The bar is `position: absolute` over the content, so nothing accounts for it
5
+ * automatically — every screen under a tab layout has to keep its own content
6
+ * out from under the pill. That number is built from the bar's private geometry
7
+ * (its expanded height and the gap it holds off the window's bottom edge),
8
+ * which is exactly why it is exported as a hook: hardcoding it in an app
9
+ * silently drifts the moment the bar changes by a pixel.
10
+ *
11
+ * Platform-neutral, like the rest of the family — `react-native-safe-area-context`
12
+ * is already a required Bloom peer and is what the bar itself measures with.
13
+ */
14
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
+
16
+ import { EXPANDED_HEIGHT, tabBarBottomGap } from './shared';
17
+
18
+ /**
19
+ * The vertical space the tab bar OCCUPIES above the bottom of the window, in
20
+ * px: its own height plus its own bottom offset, and nothing else.
21
+ *
22
+ * It is deliberately the raw footprint, with no clearance folded in. Clearance
23
+ * is a consumer decision and it differs per call site — a scroll view wants the
24
+ * footprint plus a breathing gap so the last row clears the pill, while a
25
+ * floating action button anchored by `bottom` already supplies its own gap and
26
+ * wants the footprint bare. Baking a margin in here would place that FAB too
27
+ * high and make one word mean two things inside a single file. Add your own:
28
+ *
29
+ * ```tsx
30
+ * const footprint = useTabBarFootprint();
31
+ *
32
+ * <Animated.ScrollView contentContainerStyle={{ paddingBottom: footprint + 12 }} />
33
+ * <Fab style={{ position: 'absolute', right: 16, bottom: footprint }} />
34
+ * ```
35
+ *
36
+ * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
37
+ * inset into its own gap, so this is measured from the true window edge, not
38
+ * from the safe area. Never write `footprint + insets.bottom`: that counts the
39
+ * home indicator twice and strands a visible band of dead space under every
40
+ * list (114px accounted for a 76px bar on an iOS device or PWA). Whatever you
41
+ * add on top is your own clearance — never the inset a second time.
42
+ *
43
+ * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
44
+ * down and re-expands on the way up, so measuring the minimized height would
45
+ * hide the end of a list the moment the user scrolled back to it.
46
+ */
47
+ export function useTabBarFootprint(): number {
48
+ const insets = useSafeAreaInsets();
49
+ return tabBarBottomGap(insets.bottom) + EXPANDED_HEIGHT;
50
+ }
@@ -55,6 +55,7 @@ import {
55
55
  CLOSE_BUTTON_HIT_AREA,
56
56
  ENTERING_ANIMATION_DURATION,
57
57
  STACKING_ANIMATION_DURATION,
58
+ TOAST_MAX_ROW_WIDTH,
58
59
  toastDefaults,
59
60
  } from './constants';
60
61
  import { useDynamicToastContext, useToastContext } from './context';
@@ -224,6 +225,8 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
224
225
  });
225
226
 
226
227
  const { width: screenWidth } = useWindowDimensions();
228
+ // The row is `width: '100%'` up to the cap, so this is its real width.
229
+ const rowWidth = Math.min(screenWidth, TOAST_MAX_ROW_WIDTH);
227
230
 
228
231
  const stackScaleX = useAnimatedTarget(
229
232
  calculateStackScaleX({
@@ -233,7 +236,7 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
233
236
  position,
234
237
  isExpanded,
235
238
  stackGap,
236
- screenWidth,
239
+ rowWidth,
237
240
  }),
238
241
  { duration: STACKING_ANIMATION_DURATION, easing: easeOutQuartFn },
239
242
  );
@@ -356,7 +359,9 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
356
359
  if (enableStacking && numberOfToasts > 1 && position !== 'center') {
357
360
  // On Android the RNGH tap wins the race against the nested Pressable,
358
361
  // so a press in the close-button strip has to dismiss explicitly.
359
- if (x > screenWidth - CLOSE_BUTTON_HIT_AREA) {
362
+ // `x` is relative to the row, which is capped — measuring the strip
363
+ // from the window edge would put it outside the row on a wide viewport.
364
+ if (x > rowWidth - CLOSE_BUTTON_HIT_AREA) {
360
365
  if (isExpanded && closeButton && dismissible) {
361
366
  onDismiss(id);
362
367
  }
@@ -370,7 +375,7 @@ export const ToastRow = React.forwardRef<ToastRef, ToastRowProps>(
370
375
  enableStacking,
371
376
  numberOfToasts,
372
377
  position,
373
- screenWidth,
378
+ rowWidth,
374
379
  isExpanded,
375
380
  closeButton,
376
381
  dismissible,
@@ -2,12 +2,18 @@
2
2
  * Derived from sonner-native v0.26.4 — src/gestures.tsx
3
3
  * (MIT © Gunnar Torfi Steinarsson). See the top-level NOTICE.
4
4
  *
5
- * W2 — the animated style reads `translate`, `direction`, `position`,
6
- * `windowWidth` and `isAndroid`; ALL of them are in the dependency array. On web
7
- * without the worklets babel plugin reanimated re-runs the mapper on its deps
8
- * rather than on auto-tracked reads, so a missing dep freezes the swipe transform
9
- * at its first frame (upstream omits `position` and `isAndroid`, so a toast that
10
- * changes position mid-life swipes the wrong way).
5
+ * W2 — the animated style reads `translate`, `direction`, `position`, `rowWidth`
6
+ * and `isAndroid`; ALL of them are in the dependency array. On web without the
7
+ * worklets babel plugin reanimated re-runs the mapper on its deps rather than on
8
+ * auto-tracked reads, so a missing dep freezes the swipe transform at its first
9
+ * frame (upstream omits `position` and `isAndroid`, so a toast that changes
10
+ * position mid-life swipes the wrong way).
11
+ *
12
+ * W12 — this wrapper is THE row box, so it is where `TOAST_MAX_ROW_WIDTH` caps
13
+ * and centres the toast. Capping here rather than on `ToastContent`'s card
14
+ * covers `toast.custom` JSX and `unstyled` rows too, which never reach that
15
+ * card. Every distance the gesture measures is a fraction of the capped
16
+ * `rowWidth`, not of the window — see the constant.
11
17
  */
12
18
  import * as React from 'react';
13
19
  import {
@@ -28,10 +34,11 @@ import Animated, {
28
34
  } from 'react-native-reanimated';
29
35
 
30
36
  import { easeInOutCircFn } from './animations';
37
+ import { TOAST_MAX_ROW_WIDTH } from './constants';
31
38
  import { useToastContext } from './context';
32
39
  import type { ToastPosition, ToastProps } from './types';
33
40
 
34
- /** Fraction of the screen a horizontal swipe must cross to dismiss. */
41
+ /** Fraction of the row a horizontal swipe must cross to dismiss. */
35
42
  const HORIZONTAL_DISMISS_FRACTION = 0.25;
36
43
  /** Below this the gesture reads as a tap, so the row springs back. */
37
44
  const TAP_SLOP = 16;
@@ -66,6 +73,8 @@ export const ToastSwipeHandler: React.FC<
66
73
  onPress,
67
74
  }) => {
68
75
  const { width: windowWidth } = useWindowDimensions();
76
+ // The row is `width: '100%'` up to the cap, so this is its real width.
77
+ const rowWidth = Math.min(windowWidth, TOAST_MAX_ROW_WIDTH);
69
78
  const translate = useSharedValue(0);
70
79
  const { swipeToDismissDirection: direction, position: positionCtx } =
71
80
  useToastContext();
@@ -137,7 +146,7 @@ export const ToastSwipeHandler: React.FC<
137
146
  if (direction === 'left') {
138
147
  if (Math.abs(translate.value) < TAP_SLOP) {
139
148
  springBack();
140
- } else if (translate.value < -windowWidth * HORIZONTAL_DISMISS_FRACTION) {
149
+ } else if (translate.value < -rowWidth * HORIZONTAL_DISMISS_FRACTION) {
141
150
  dismiss();
142
151
  } else {
143
152
  springBack();
@@ -175,11 +184,11 @@ export const ToastSwipeHandler: React.FC<
175
184
  ? 1
176
185
  : interpolate(
177
186
  translate.value,
178
- [0, direction === 'left' ? -windowWidth : -VERTICAL_FADE_DISTANCE],
187
+ [0, direction === 'left' ? -rowWidth : -VERTICAL_FADE_DISTANCE],
179
188
  [1, 0],
180
189
  ),
181
190
  };
182
- }, [direction, translate, windowWidth, position, isAndroid]);
191
+ }, [direction, translate, rowWidth, position, isAndroid]);
183
192
 
184
193
  return (
185
194
  <GestureDetector gesture={Gesture.Race(tap, pan)}>
@@ -187,7 +196,7 @@ export const ToastSwipeHandler: React.FC<
187
196
  style={[
188
197
  animatedStyle,
189
198
  unstyled ? undefined : styles.centered,
190
- styles.fullWidth,
199
+ styles.rowBox,
191
200
  style,
192
201
  ]}
193
202
  // W9 — on web `LinearTransition`'s easing degrades to CSS `ease` because
@@ -205,7 +214,16 @@ export const ToastSwipeHandler: React.FC<
205
214
 
206
215
  const styles = StyleSheet.create({
207
216
  centered: { justifyContent: 'center' },
208
- fullWidth: { width: '100%' },
217
+ /**
218
+ * `alignSelf` is load-bearing: the parent anchor stretches its children, but a
219
+ * child with a definite cross size (`width: '100%'` capped by `maxWidth`) is
220
+ * not stretched — it would sit against the left edge without this.
221
+ */
222
+ rowBox: {
223
+ width: '100%',
224
+ maxWidth: TOAST_MAX_ROW_WIDTH,
225
+ alignSelf: 'center',
226
+ },
209
227
  });
210
228
 
211
229
  /**
@@ -6,6 +6,7 @@
6
6
  * `toast-store.ts` never has to import a Reanimated-dependent module: the store
7
7
  * stays pure JS and its suite needs zero React Native mocks.
8
8
  */
9
+ import { space } from '../styles/tokens';
9
10
  import type {
10
11
  AutoWiggle,
11
12
  ToastPosition,
@@ -21,6 +22,33 @@ export const OUTSIDE_PRESS_PADDING = 20;
21
22
  /** However deep a stack gets, a buried row never narrows past this scale. */
22
23
  export const MIN_STACK_SCALE_X = 0.8;
23
24
 
25
+ /**
26
+ * W12 — HOW WIDE A TOAST ROW IS ALLOWED TO GET.
27
+ *
28
+ * sonner-native has no cap: the row is `width: '100%'` and the card inside it is
29
+ * inset by `marginHorizontal: space.lg`, so the card is always
30
+ * screen-width-minus-32. That is correct for the platform it was written for and
31
+ * wrong everywhere else — on a 1280px desktop viewport it produces a ~1248px
32
+ * card, which reads as a page banner rather than a toast. sonner (web) instead
33
+ * fixes the card at `--width: 356px`.
34
+ *
35
+ * The cap therefore sizes the ROW BOX, which carries the card's two `space.lg`
36
+ * gutters, so the visible card lands on sonner's 356px on any viewport wide
37
+ * enough to reach it. `maxWidth` needs no platform fork because it degrades by
38
+ * itself: below a 388px viewport the row is still screen-width and the card is
39
+ * still screen-width-minus-32, exactly as before, so phone portrait — the case
40
+ * the mobile sizing was right for — is untouched. Above it (desktop, tablet,
41
+ * phone landscape) the row caps and centres.
42
+ *
43
+ * `ToastSwipeHandler`'s dismiss threshold and `calculateStackScaleX`'s depth cue
44
+ * are fractions of this row width, NOT of the window: at 1280px a
45
+ * window-relative threshold would demand a 320px drag from a 388px card, and a
46
+ * window-relative squeeze would shrink a buried row by 4px instead of 16px.
47
+ * A consumer who overrides the cap through `toastOptions.toastContainerStyle`
48
+ * moves the card only; both of those keep measuring against this default.
49
+ */
50
+ export const TOAST_MAX_ROW_WIDTH = 356 + space.lg * 2;
51
+
24
52
  /** Enter animation length; also the auto-close timer's head start and the overlay teardown delay. */
25
53
  export const ENTERING_ANIMATION_DURATION = 300;
26
54
  export const STACKING_ANIMATION_DURATION = 600;
@@ -34,7 +34,12 @@ export const getOrderedToastIds = (
34
34
  *
35
35
  * Scale rather than width: changing the real width would rewrap the text. Returns
36
36
  * 1 (no squeeze) for a single row, an expanded stack, stacking turned off, or
37
- * before the window width is known.
37
+ * before the row width is known.
38
+ *
39
+ * `rowWidth` is the row's OWN width, not the window's. Upstream can use the
40
+ * screen width because its row is always full-bleed; Bloom's is capped at
41
+ * `TOAST_MAX_ROW_WIDTH`, and dividing a fixed `stackGap` inset by a 1280px window
42
+ * would shrink a buried row by 4 visible pixels instead of the intended 16.
38
43
  */
39
44
  export const calculateStackScaleX = ({
40
45
  index,
@@ -43,7 +48,7 @@ export const calculateStackScaleX = ({
43
48
  position,
44
49
  isExpanded,
45
50
  stackGap,
46
- screenWidth,
51
+ rowWidth,
47
52
  }: {
48
53
  index: number;
49
54
  numberOfToasts: number;
@@ -51,16 +56,16 @@ export const calculateStackScaleX = ({
51
56
  position: ToastPosition;
52
57
  isExpanded: boolean;
53
58
  stackGap: number;
54
- screenWidth: number;
59
+ rowWidth: number;
55
60
  }): number => {
56
- if (!enableStacking || numberOfToasts <= 1 || isExpanded || screenWidth <= 0) {
61
+ if (!enableStacking || numberOfToasts <= 1 || isExpanded || rowWidth <= 0) {
57
62
  return 1;
58
63
  }
59
64
 
60
65
  const distanceFromFront =
61
66
  position === 'top-center' ? index : numberOfToasts - index - 1;
62
67
  const narrowAmount = stackGap * distanceFromFront * 2;
63
- return Math.max(MIN_STACK_SCALE_X, 1 - narrowAmount / screenWidth);
68
+ return Math.max(MIN_STACK_SCALE_X, 1 - narrowAmount / rowWidth);
64
69
  };
65
70
 
66
71
  const heightOf = (
@@ -36,7 +36,7 @@ import {
36
36
  ArrowOutOfBox_Stroke2_Corner0_Rounded,
37
37
  } from '../icons';
38
38
  import { Z_INDEX } from '../styles/z-index';
39
- import { WEB_POSITION_FIXED } from '../styles/web-view-style';
39
+ import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
40
40
  import {
41
41
  getAspectRatio,
42
42
  fetchAspectRatio,
@@ -76,14 +76,15 @@ const AnimatedImage = Animated.createAnimatedComponent(Image);
76
76
 
77
77
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
78
78
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
79
- // image. `Platform.select` keeps them off native; the `as ViewStyle` cast
80
- // mirrors Bloom's other web-only style casts (e.g. Dialog.web's `animation`).
81
- const webPointerStyle = Platform.select({
82
- web: { userSelect: 'none', cursor: 'pointer' } as ViewStyle,
83
- default: undefined,
84
- });
79
+ // image. `Platform.select` keeps them off native. `cursor` is a real
80
+ // `ViewStyle` key on RN 0.83; `userSelect` is declared on `TextStyle` only,
81
+ // which is what `WebCssStyle` covers.
82
+ const WEB_POINTER: WebCssStyle = { userSelect: 'none', cursor: 'pointer' };
83
+ const WEB_USER_SELECT_NONE: WebCssStyle = { userSelect: 'none' };
84
+
85
+ const webPointerStyle = Platform.select({ web: WEB_POINTER, default: undefined });
85
86
  const webUserSelectNoneStyle = Platform.select({
86
- web: { userSelect: 'none' } as ViewStyle,
87
+ web: WEB_USER_SELECT_NONE,
87
88
  default: undefined,
88
89
  });
89
90
 
@@ -1,50 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.useTabBarReservedSpace = useTabBarReservedSpace;
7
- var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
8
- var _shared = require("./shared.js");
9
- /**
10
- * How much room a screen must leave at the bottom for the floating bar.
11
- *
12
- * The bar is `position: absolute` over the content, so nothing reserves space
13
- * for it automatically — every scrollable screen under a tab layout has to pad
14
- * its own content out from under the pill. That number is built from the bar's
15
- * private geometry (its expanded height and the gap it keeps from the window's
16
- * bottom edge), which is exactly why it is exported as a hook: hardcoding it in
17
- * an app silently drifts the moment the bar changes by a pixel.
18
- *
19
- * Platform-neutral, like the rest of the family — `react-native-safe-area-context`
20
- * is already a required Bloom peer and is what the bar itself measures with.
21
- */
22
-
23
- /**
24
- * The vertical space the tab bar occupies above the bottom of the window, in
25
- * px. Use it directly as the bottom padding (or bottom offset) of anything that
26
- * would otherwise sit under the bar:
27
- *
28
- * ```tsx
29
- * const reserved = useTabBarReservedSpace();
30
- *
31
- * <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
32
- * <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
33
- * ```
34
- *
35
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
36
- * inset into its own bottom gap, so it is measured from the true window edge,
37
- * not from the safe area. Never write `reserved + insets.bottom`: that counts
38
- * the home indicator twice and strands a visible band of dead space under every
39
- * list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
40
- * number; nothing needs adding to it.
41
- *
42
- * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
43
- * down and re-expands on the way up, so reserving the minimized height would
44
- * hide the end of a list the moment the user scrolled back to it.
45
- */
46
- function useTabBarReservedSpace() {
47
- const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
48
- return (0, _shared.tabBarBottomGap)(insets.bottom) + _shared.EXPANDED_HEIGHT;
49
- }
50
- //# sourceMappingURL=use-reserved-space.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_reactNativeSafeAreaContext","require","_shared","useTabBarReservedSpace","insets","useSafeAreaInsets","tabBarBottomGap","bottom","EXPANDED_HEIGHT"],"sourceRoot":"../../../src","sources":["tab-bar/use-reserved-space.ts"],"mappings":";;;;;;AAaA,IAAAA,2BAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AAfA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASE,sBAAsBA,CAAA,EAAW;EAC/C,MAAMC,MAAM,GAAG,IAAAC,6CAAiB,EAAC,CAAC;EAClC,OAAO,IAAAC,uBAAe,EAACF,MAAM,CAACG,MAAM,CAAC,GAAGC,uBAAe;AACzD","ignoreList":[]}
@@ -1,46 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * How much room a screen must leave at the bottom for the floating bar.
5
- *
6
- * The bar is `position: absolute` over the content, so nothing reserves space
7
- * for it automatically — every scrollable screen under a tab layout has to pad
8
- * its own content out from under the pill. That number is built from the bar's
9
- * private geometry (its expanded height and the gap it keeps from the window's
10
- * bottom edge), which is exactly why it is exported as a hook: hardcoding it in
11
- * an app silently drifts the moment the bar changes by a pixel.
12
- *
13
- * Platform-neutral, like the rest of the family — `react-native-safe-area-context`
14
- * is already a required Bloom peer and is what the bar itself measures with.
15
- */
16
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
- import { EXPANDED_HEIGHT, tabBarBottomGap } from "./shared.js";
18
-
19
- /**
20
- * The vertical space the tab bar occupies above the bottom of the window, in
21
- * px. Use it directly as the bottom padding (or bottom offset) of anything that
22
- * would otherwise sit under the bar:
23
- *
24
- * ```tsx
25
- * const reserved = useTabBarReservedSpace();
26
- *
27
- * <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
28
- * <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
29
- * ```
30
- *
31
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
32
- * inset into its own bottom gap, so it is measured from the true window edge,
33
- * not from the safe area. Never write `reserved + insets.bottom`: that counts
34
- * the home indicator twice and strands a visible band of dead space under every
35
- * list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
36
- * number; nothing needs adding to it.
37
- *
38
- * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
39
- * down and re-expands on the way up, so reserving the minimized height would
40
- * hide the end of a list the moment the user scrolled back to it.
41
- */
42
- export function useTabBarReservedSpace() {
43
- const insets = useSafeAreaInsets();
44
- return tabBarBottomGap(insets.bottom) + EXPANDED_HEIGHT;
45
- }
46
- //# sourceMappingURL=use-reserved-space.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["useSafeAreaInsets","EXPANDED_HEIGHT","tabBarBottomGap","useTabBarReservedSpace","insets","bottom"],"sourceRoot":"../../../src","sources":["tab-bar/use-reserved-space.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,iBAAiB,QAAQ,gCAAgC;AAElE,SAASC,eAAe,EAAEC,eAAe,QAAQ,aAAU;;AAE3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,sBAAsBA,CAAA,EAAW;EAC/C,MAAMC,MAAM,GAAGJ,iBAAiB,CAAC,CAAC;EAClC,OAAOE,eAAe,CAACE,MAAM,CAACC,MAAM,CAAC,GAAGJ,eAAe;AACzD","ignoreList":[]}
@@ -1,25 +0,0 @@
1
- /**
2
- * The vertical space the tab bar occupies above the bottom of the window, in
3
- * px. Use it directly as the bottom padding (or bottom offset) of anything that
4
- * would otherwise sit under the bar:
5
- *
6
- * ```tsx
7
- * const reserved = useTabBarReservedSpace();
8
- *
9
- * <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
10
- * <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
11
- * ```
12
- *
13
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
14
- * inset into its own bottom gap, so it is measured from the true window edge,
15
- * not from the safe area. Never write `reserved + insets.bottom`: that counts
16
- * the home indicator twice and strands a visible band of dead space under every
17
- * list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
18
- * number; nothing needs adding to it.
19
- *
20
- * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
21
- * down and re-expands on the way up, so reserving the minimized height would
22
- * hide the end of a list the moment the user scrolled back to it.
23
- */
24
- export declare function useTabBarReservedSpace(): number;
25
- //# sourceMappingURL=use-reserved-space.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-reserved-space.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-reserved-space.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C"}
@@ -1,25 +0,0 @@
1
- /**
2
- * The vertical space the tab bar occupies above the bottom of the window, in
3
- * px. Use it directly as the bottom padding (or bottom offset) of anything that
4
- * would otherwise sit under the bar:
5
- *
6
- * ```tsx
7
- * const reserved = useTabBarReservedSpace();
8
- *
9
- * <Animated.ScrollView contentContainerStyle={{ paddingBottom: reserved }} />
10
- * <Fab style={{ position: 'absolute', right: 16, bottom: reserved + 12 }} />
11
- * ```
12
- *
13
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
14
- * inset into its own bottom gap, so it is measured from the true window edge,
15
- * not from the safe area. Never write `reserved + insets.bottom`: that counts
16
- * the home indicator twice and strands a visible band of dead space under every
17
- * list (a 76px bar reserving 110px on an iOS device or PWA). It is the whole
18
- * number; nothing needs adding to it.
19
- *
20
- * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
21
- * down and re-expands on the way up, so reserving the minimized height would
22
- * hide the end of a list the moment the user scrolled back to it.
23
- */
24
- export declare function useTabBarReservedSpace(): number;
25
- //# sourceMappingURL=use-reserved-space.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use-reserved-space.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-reserved-space.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAG/C"}
@@ -1,150 +0,0 @@
1
- import React from 'react';
2
- import type { ReactTestInstance } from 'react-test-renderer';
3
- import { render } from '@testing-library/react-native';
4
-
5
- import { BloomThemeProvider } from '../theme/BloomThemeProvider';
6
- import { TabBar, TabBarButton, useTabBarReservedSpace } from '../tab-bar';
7
- import type { TabBarItem } from '../tab-bar/types';
8
-
9
- /**
10
- * `useTabBarReservedSpace()` — the number a screen pads its content by so a
11
- * list does not end underneath the floating bar.
12
- *
13
- * The bar is absolutely positioned, so nothing reserves that space
14
- * automatically, and the geometry it is built from (expanded height, bottom
15
- * gap) is private. Without the hook a consumer hardcodes both — which drifts
16
- * silently the first time the bar changes by a pixel — and, worse, tends to add
17
- * `insets.bottom` on top, because that is what reserving space for bottom
18
- * chrome normally requires. The bar already folds the inset into its own gap,
19
- * so that double-counts the home indicator.
20
- *
21
- * The suite therefore asserts three separate things: the absolute pixel values
22
- * (so a change to the constants must be deliberate), the inset relationship (so
23
- * the double-count can never creep back in), and equality with the bar's own
24
- * rendered geometry (so the hook cannot drift from where the bar actually sits).
25
- */
26
-
27
- // Mutated per test; must be `mock`-prefixed for the hoisted factory.
28
- const mockInsets = { top: 0, right: 0, bottom: 0, left: 0 };
29
-
30
- jest.mock('react-native-safe-area-context', () => ({
31
- ...jest.requireActual('react-native-safe-area-context'),
32
- useSafeAreaInsets: () => mockInsets,
33
- }));
34
-
35
- const ITEMS: TabBarItem[] = [
36
- { name: 'home', label: 'Home', icon: null },
37
- { name: 'search', label: 'Search', icon: null },
38
- ];
39
-
40
- /** A device with a home indicator (iPhone-class bottom inset). */
41
- const HOME_INDICATOR_INSET = 34;
42
-
43
- function ReservedProbe({ onValue }: { onValue: (space: number) => void }) {
44
- onValue(useTabBarReservedSpace());
45
- return null;
46
- }
47
-
48
- function withTheme(ui: React.ReactElement) {
49
- return (
50
- <BloomThemeProvider mode="light" colorPreset="teal">
51
- {ui}
52
- </BloomThemeProvider>
53
- );
54
- }
55
-
56
- /** The space the hook reserves on a device whose bottom inset is `inset`. */
57
- function reservedFor(inset: number): number {
58
- mockInsets.bottom = inset;
59
- let space: number | undefined;
60
- render(withTheme(<ReservedProbe onValue={(value) => (space = value)} />));
61
- if (space === undefined) throw new Error('ReservedProbe never ran');
62
- return space;
63
- }
64
-
65
- function flattenStyle(style: unknown): Record<string, unknown> {
66
- const out: Record<string, unknown> = {};
67
- const visit = (value: unknown): void => {
68
- if (Array.isArray(value)) value.forEach(visit);
69
- else if (value && typeof value === 'object') Object.assign(out, value);
70
- };
71
- visit(style);
72
- return out;
73
- }
74
-
75
- /**
76
- * The flattened style of every styled node in the tree.
77
- *
78
- * Restricted to HOST nodes: the RN mock renders each component as a composite
79
- * wrapping a host element of the same name, and both carry the same `style`, so
80
- * counting all of them would double every match.
81
- */
82
- function styledNodes(root: ReactTestInstance): Record<string, unknown>[] {
83
- return root
84
- .findAll((node) => typeof node.type === 'string' && node.props?.style !== undefined)
85
- .map((node) => flattenStyle(node.props.style));
86
- }
87
-
88
- describe('useTabBarReservedSpace', () => {
89
- beforeEach(() => {
90
- mockInsets.bottom = 0;
91
- });
92
-
93
- it('reserves the bar plus its gap where there is no bottom inset', () => {
94
- // 58pt bar + the 12pt floor the gap never goes below.
95
- expect(reservedFor(0)).toBe(70);
96
- });
97
-
98
- it('reserves more on a device with a home indicator', () => {
99
- expect(reservedFor(HOME_INDICATOR_INSET)).toBe(76);
100
- });
101
-
102
- it('already includes the inset — adding insets.bottom would double-count it', () => {
103
- // The bar absorbs 16pt of the inset into its own gap, so the space grows by
104
- // the REMAINDER, never by the whole inset. A consumer writing
105
- // `reserved + insets.bottom` would reserve 110pt for a 76pt bar.
106
- const grown = reservedFor(HOME_INDICATOR_INSET) - reservedFor(0);
107
- expect(grown).toBe(6);
108
- expect(grown).toBeLessThan(HOME_INDICATOR_INSET);
109
- });
110
-
111
- it('does not shrink below the floor for a small inset', () => {
112
- // An inset under what the gap absorbs must not pull the bar down toward the
113
- // window edge — the gap is clamped, and the reserved space with it.
114
- expect(reservedFor(8)).toBe(reservedFor(0));
115
- });
116
-
117
- it('equals the space the rendered bar actually occupies', () => {
118
- // The invariant that makes the hook worth exporting: its number is the bar's
119
- // own geometry (the gap it holds off the window edge plus its expanded
120
- // height), not a second copy that can drift.
121
- mockInsets.bottom = HOME_INDICATOR_INSET;
122
- let reserved: number | undefined;
123
- const { UNSAFE_root } = render(
124
- withTheme(
125
- <>
126
- <ReservedProbe onValue={(value) => (reserved = value)} />
127
- <TabBar activeIndex={0}>
128
- {ITEMS.map((item, index) => (
129
- <TabBarButton key={item.name} item={item} index={index} />
130
- ))}
131
- </TabBar>
132
- </>,
133
- ),
134
- );
135
-
136
- const styles = styledNodes(UNSAFE_root);
137
- // The wrapper holding the pill off the bottom edge — the only node with a
138
- // bottom margin.
139
- const gaps = styles.filter((style) => typeof style.marginBottom === 'number');
140
- expect(gaps).toHaveLength(1);
141
- // The pill itself: the animated capsule sizes BOTH its height and its
142
- // horizontal inset, which nothing else in the bar does.
143
- const pills = styles.filter(
144
- (style) => typeof style.height === 'number' && typeof style.marginHorizontal === 'number',
145
- );
146
- expect(pills).toHaveLength(1);
147
-
148
- expect(reserved).toBe(Number(gaps[0]?.marginBottom) + Number(pills[0]?.height));
149
- });
150
- });