@oxyhq/bloom 0.52.0 → 0.52.1

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
@@ -1,6 +1,7 @@
1
1
  import React, { memo, useEffect, useState } from 'react';
2
2
  import { View, type ViewStyle } from 'react-native';
3
3
 
4
+ import type { WebCssStyle } from '../styles/web-view-style';
4
5
  import { useTheme } from '../theme/use-theme';
5
6
  import { ConnectionDotsBase } from './ConnectionDotsBase';
6
7
  import type { ConnectionDotsProps } from './types';
@@ -88,14 +89,14 @@ const ConnectionDotsComponent: React.FC<ConnectionDotsProps> = ({
88
89
  borderRadius: dotSize / 2,
89
90
  backgroundColor: color,
90
91
  };
91
- const animated: ViewStyle = reduce
92
+ const animated: WebCssStyle = reduce
92
93
  ? { opacity: 1 }
93
- : ({
94
+ : {
94
95
  // `animation` / `animationDelay` are real CSS props on web;
95
96
  // react-native-web passes them through to the DOM node.
96
97
  animation: `${WAVE_ANIMATION_NAME} ${WAVE_DURATION_MS}ms linear infinite`,
97
98
  animationDelay: `${(i / count) * WAVE_DURATION_MS}ms`,
98
- } as ViewStyle);
99
+ };
99
100
  return (
100
101
  <View
101
102
  key={i}
@@ -36,7 +36,11 @@ export const SHADOW_BOX: Record<ShadowRole, string> = {
36
36
  * On WEB this returns a `{ boxShadow }` style object (so it can also be applied
37
37
  * inline without Tailwind). The `.native.ts` fork returns RN
38
38
  * `shadowColor/shadowOffset/shadowOpacity/shadowRadius/elevation`.
39
+ *
40
+ * No widening needed: `boxShadow` is a first-class `ViewStyle` key on RN >= 0.76
41
+ * and takes a CSS string, so this is not one of the web-only properties
42
+ * `styles/web-view-style.ts` exists for.
39
43
  */
40
44
  export function bloomShadowStyle(role: ShadowRole): ViewStyle {
41
- return { boxShadow: SHADOW_BOX[role] } as ViewStyle;
45
+ return { boxShadow: SHADOW_BOX[role] };
42
46
  }
@@ -515,7 +515,7 @@ function SideSheet({
515
515
  [side]: anchorInset,
516
516
  width: sideWidth,
517
517
  borderRadius: PANEL_RADIUS,
518
- }) as ViewStyle,
518
+ }),
519
519
  [insetTop, insetBottom, side, anchorInset, sideWidth],
520
520
  );
521
521
 
@@ -21,7 +21,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
21
21
 
22
22
  import { Portal } from '../portal/index.web';
23
23
  import { createOverlayZIndex } from '../styles/z-index';
24
- import { WEB_POSITION_FIXED } from '../styles/web-view-style';
24
+ import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
25
25
  import { bloomShadowStyle } from '../design-tokens/shadows';
26
26
  import { useTheme } from '../theme/use-theme';
27
27
  import { Context, useDialogContext, useDialogControl } from './context';
@@ -59,6 +59,23 @@ import type {
59
59
 
60
60
  const FADE_OUT_DURATION = CENTER_FADE_OUT_DURATION;
61
61
 
62
+ /**
63
+ * The four CSS `animation` shorthands driving the centered card and its backdrop.
64
+ * `animation` has no `ViewStyle` key at all — react-native-web forwards it to the
65
+ * DOM node — so these are annotated `WebCssStyle` rather than cast. The
66
+ * `@keyframes` they name are self-injected by `useDialogCss()`.
67
+ */
68
+ const ZOOM_FADE_IN: WebCssStyle = {
69
+ animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s',
70
+ };
71
+ const ZOOM_FADE_OUT: WebCssStyle = {
72
+ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards`,
73
+ };
74
+ const BACKDROP_FADE_IN: WebCssStyle = { animation: 'bloomDialogFadeIn ease-out 0.15s' };
75
+ const BACKDROP_FADE_OUT: WebCssStyle = {
76
+ animation: `bloomDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards`,
77
+ };
78
+
62
79
  const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagation();
63
80
 
64
81
  const ClosingContext = createContext(false);
@@ -475,9 +492,7 @@ function DialogPanel({
475
492
  ...bloomShadowStyle('m'),
476
493
  zIndex: surfaceZIndex,
477
494
  },
478
- isClosing
479
- ? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
480
- : ({ animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s' } as ViewStyle),
495
+ isClosing ? ZOOM_FADE_OUT : ZOOM_FADE_IN,
481
496
  // Drives `height` (and `maxWidth`) only while a morph is in flight; at
482
497
  // rest it resolves to `height: 'auto'` — the natural sizing above. Placed
483
498
  // before `style` so a consumer's explicit size still wins.
@@ -625,23 +640,21 @@ function SheetSurface({
625
640
 
626
641
  const visible = shown && entered;
627
642
 
628
- const panelTransition = useMemo<ViewStyle>(
629
- () =>
630
- ({
631
- transitionProperty: 'transform, opacity',
632
- transitionDuration: `${ANIMATION_DURATION}ms`,
633
- transitionTimingFunction: EASE_OUT,
634
- }) as ViewStyle,
643
+ const panelTransition = useMemo<WebCssStyle>(
644
+ () => ({
645
+ transitionProperty: 'transform, opacity',
646
+ transitionDuration: `${ANIMATION_DURATION}ms`,
647
+ transitionTimingFunction: EASE_OUT,
648
+ }),
635
649
  [],
636
650
  );
637
651
 
638
- const backdropTransition = useMemo<ViewStyle>(
639
- () =>
640
- ({
641
- transitionProperty: 'opacity',
642
- transitionDuration: `${ANIMATION_DURATION}ms`,
643
- transitionTimingFunction: EASE_OUT,
644
- }) as ViewStyle,
652
+ const backdropTransition = useMemo<WebCssStyle>(
653
+ () => ({
654
+ transitionProperty: 'opacity',
655
+ transitionDuration: `${ANIMATION_DURATION}ms`,
656
+ transitionTimingFunction: EASE_OUT,
657
+ }),
645
658
  [],
646
659
  );
647
660
 
@@ -664,7 +677,7 @@ function SheetSurface({
664
677
  borderRadius: PANEL_RADIUS,
665
678
  transform: [{ translateX: visible ? 0 : hiddenSign }],
666
679
  opacity: visible ? 1 : 0,
667
- } as ViewStyle;
680
+ };
668
681
  }, [visible, placement, width, inset, viewportWidth]);
669
682
 
670
683
  const handleBackdropPress = useCallback(() => {
@@ -798,9 +811,7 @@ function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
798
811
  bottom: 0,
799
812
  backgroundColor: 'rgba(0,0,0,0.8)',
800
813
  },
801
- isClosing
802
- ? ({ animation: `bloomDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
803
- : ({ animation: 'bloomDialogFadeIn ease-out 0.15s' } as ViewStyle),
814
+ isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
804
815
  ];
805
816
 
806
817
  return <View style={style} />;
@@ -839,7 +850,11 @@ export function AutoMountedDialog({
839
850
  );
840
851
  }
841
852
 
842
- const sheetStyles = {
853
+ // Annotated rather than cast: a plain object literal widens `position: 'absolute'`
854
+ // to `string`, which is what the three `as ViewStyle` casts here used to silence.
855
+ // The annotation supplies the contextual type instead, so the literals narrow and
856
+ // a genuine mistake still fails.
857
+ const sheetStyles: Record<'root' | 'backdrop' | 'panel', ViewStyle> = {
843
858
  // `zIndex` for the root (backdrop) and panel (surface) is applied inline from
844
859
  // the per-layer `createOverlayZIndex(layer)` in `SheetSurface`, so a side
845
860
  // dialog stacked on top of another (surface stack) paints above it.
@@ -849,7 +864,7 @@ const sheetStyles = {
849
864
  left: 0,
850
865
  right: 0,
851
866
  bottom: 0,
852
- } as ViewStyle,
867
+ },
853
868
  backdrop: {
854
869
  position: 'absolute',
855
870
  top: 0,
@@ -857,13 +872,13 @@ const sheetStyles = {
857
872
  right: 0,
858
873
  bottom: 0,
859
874
  backgroundColor: '#000',
860
- } as ViewStyle,
875
+ },
861
876
  panel: {
862
877
  position: 'absolute',
863
878
  overflow: 'hidden',
864
879
  // Elevation is applied at the usage site via `bloomShadowStyle('m')`
865
880
  // (`boxShadow` on web); the deprecated `shadow*` props were removed.
866
- } as ViewStyle,
881
+ },
867
882
  };
868
883
 
869
884
  /**
package/src/index.web.ts CHANGED
@@ -146,8 +146,8 @@ export { SubtleHover } from './subtle-hover';
146
146
  export type { SubtleHoverProps } from './subtle-hover';
147
147
 
148
148
  // Motion presets (Reanimated enter/exit + directional screen transition)
149
- export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion';
150
- export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion';
149
+ export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion/index.web';
150
+ export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion/index.web';
151
151
  export { AnimatedCheck } from './animated-check';
152
152
  export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
153
153
 
@@ -1,5 +1,7 @@
1
1
  import React, { memo, useMemo } from 'react';
2
- import { View, Text, StyleSheet, type DimensionValue, type ViewStyle } from 'react-native';
2
+ import { View, Text, StyleSheet, type DimensionValue } from 'react-native';
3
+
4
+ import type { WebCssStyle } from '../styles/web-view-style';
3
5
 
4
6
  import { useTheme } from '../theme/use-theme';
5
7
  import { animation } from '../styles/tokens';
@@ -87,17 +89,17 @@ const TopLoading: React.FC<TopLoadingProps> = ({
87
89
  // reanimated's `Easing.out(Easing.cubic)` used by the native variant.
88
90
  const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
89
91
 
90
- const containerTransition = {
92
+ const containerTransition: WebCssStyle = {
91
93
  transitionProperty: 'height',
92
94
  transitionDuration: `${duration}ms`,
93
95
  transitionTimingFunction: easing,
94
- } as ViewStyle;
96
+ };
95
97
 
96
- const innerTransition = {
98
+ const innerTransition: WebCssStyle = {
97
99
  transitionProperty: 'opacity, transform',
98
100
  transitionDuration: `${duration}ms`,
99
101
  transitionTimingFunction: easing,
100
- } as ViewStyle;
102
+ };
101
103
 
102
104
  return (
103
105
  <View
@@ -1,6 +1,8 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { StyleSheet, View, type ViewStyle } from 'react-native';
3
3
 
4
+ import type { WebCssStyle } from '../styles/web-view-style';
5
+
4
6
  interface SpinnerIconProps {
5
7
  size?: number;
6
8
  color?: string;
@@ -59,6 +61,15 @@ export const BLOOM_SPINNER_CSS = `
59
61
  @keyframes ${SPIN_ANIMATION_NAME} { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
60
62
  `;
61
63
 
64
+ /**
65
+ * `animation` is a real CSS prop on web; react-native-web passes it through to
66
+ * the DOM node. RN's `ViewStyle` has no `animation` key, which is what
67
+ * `WebCssStyle` covers.
68
+ */
69
+ const SPIN_STYLE: WebCssStyle = {
70
+ animation: `${SPIN_ANIMATION_NAME} ${SPIN_DURATION_MS}ms linear infinite`,
71
+ };
72
+
62
73
  function useKeyframes(): void {
63
74
  useEffect(() => {
64
75
  if (typeof document === 'undefined') return;
@@ -87,15 +98,7 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
87
98
  return (
88
99
  <View
89
100
  {...(className ? ({ className } as Record<string, string>) : {})}
90
- // `animation` is a real CSS prop on web; react-native-web passes it
91
- // through to the DOM node. Typed as a cast because RN's ViewStyle has no
92
- // `animation` key.
93
- style={[
94
- styles.container,
95
- { width: size, height: size },
96
- { animation: `${SPIN_ANIMATION_NAME} ${SPIN_DURATION_MS}ms linear infinite` } as ViewStyle,
97
- style,
98
- ]}
101
+ style={[styles.container, { width: size, height: size }, SPIN_STYLE, style]}
99
102
  >
100
103
  {BLADES.map(({ angle, opacity }) => (
101
104
  <View
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Web variant of the `motion` barrel. Only the three layout-animation presets
3
+ * fork — see `motion.web.ts` for why. `ScreenTransition` is universal: it already
4
+ * branches on `Platform.OS` internally.
5
+ */
6
+ export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop } from './motion.web';
7
+ export { ScreenTransition } from './ScreenTransition';
8
+ export type { ScreenTransitionProps, ScreenTransitionDirection } from './ScreenTransition';
@@ -2,17 +2,25 @@ import {
2
2
  withDelay,
3
3
  withSequence,
4
4
  withTiming,
5
- type LayoutAnimation,
5
+ type EntryOrExitLayoutType,
6
6
  } from 'react-native-reanimated';
7
7
 
8
8
  /**
9
- * Reanimated custom layout-animation presets. Each is a worklet builder
10
- * compatible with an `Animated.View`'s `entering` / `exiting` prop:
9
+ * Reanimated layout-animation presets. Each is a value you hand to an
10
+ * `Animated.View`'s `entering` / `exiting` prop:
11
11
  *
12
12
  * ```tsx
13
13
  * <Animated.View entering={ScaleAndFadeIn} exiting={ScaleAndFadeOut} />
14
14
  * ```
15
15
  *
16
+ * THIS IS THE NATIVE FILE. Custom worklet builders animate correctly on native
17
+ * and do NOTHING on web, where reanimated resolves an animation by preset name —
18
+ * `motion.web.ts` reimplements all three against the mechanisms web actually
19
+ * supports and documents why the two platforms cannot share one. The exported
20
+ * TYPE is deliberately reanimated's own "thing you can pass to entering/exiting"
21
+ * union rather than the concrete builder shape, so the two files present one
22
+ * contract and no consumer can depend on the mechanism.
23
+ *
16
24
  * Ported from Bluesky's custom-animation set. The `'worklet'` directive is
17
25
  * preserved through Bloom's build; the consuming app's Metro + reanimated Babel
18
26
  * plugin transforms it so the animation runs on the UI thread (same path the
@@ -20,7 +28,7 @@ import {
20
28
  */
21
29
 
22
30
  /** Entering: fade in while growing from 70% to full size. */
23
- export const ScaleAndFadeIn: () => LayoutAnimation = () => {
31
+ export const ScaleAndFadeIn: EntryOrExitLayoutType = () => {
24
32
  'worklet';
25
33
  return {
26
34
  animations: {
@@ -35,7 +43,7 @@ export const ScaleAndFadeIn: () => LayoutAnimation = () => {
35
43
  };
36
44
 
37
45
  /** Exiting counterpart to {@link ScaleAndFadeIn}: fade out while shrinking to 70%. */
38
- export const ScaleAndFadeOut: () => LayoutAnimation = () => {
46
+ export const ScaleAndFadeOut: EntryOrExitLayoutType = () => {
39
47
  'worklet';
40
48
  return {
41
49
  animations: {
@@ -53,7 +61,7 @@ export const ScaleAndFadeOut: () => LayoutAnimation = () => {
53
61
  * Exiting with a brief "pop": the scale dips to 70%, overshoots to 110%, and the
54
62
  * element fades out. Good for confirming a tapped or removed item.
55
63
  */
56
- export const ShrinkAndPop: () => LayoutAnimation = () => {
64
+ export const ShrinkAndPop: EntryOrExitLayoutType = () => {
57
65
  'worklet';
58
66
  return {
59
67
  animations: {
@@ -0,0 +1,99 @@
1
+ /**
2
+ * WEB FORK of `motion.ts`. Same three names, same contract ("a value you hand to
3
+ * an `Animated.View`'s `entering` / `exiting`"), different mechanism — because on
4
+ * web the native mechanism does nothing at all and the obvious replacement is
5
+ * worse than nothing.
6
+ *
7
+ * WHY THE DEFAULT FILE CANNOT SERVE WEB. `motion.ts` exports custom
8
+ * `EntryExitAnimationFunction` worklet builders. Reanimated's web layout-animation
9
+ * manager resolves an animation by NAME: `tryGetAnimationConfig` reads
10
+ * `config.presetName` off the function, a custom builder has none, and
11
+ * `checkUndefinedAnimationFail` then bails to `makeElementVisible(element, 0)`.
12
+ * Measured in Chrome 5.x/reanimated 4.5.0: the card mounts at `transform: none`,
13
+ * `opacity: 1` and stays there for every sampled frame of both directions, with
14
+ * one `[Reanimated] Couldn't load entering/exiting animation` warning per element.
15
+ * Native is unaffected — it runs the builder directly, with no name lookup.
16
+ *
17
+ * WHY THE ENTER AND THE EXITS USE DIFFERENT MECHANISMS. Rebuilding all three as
18
+ * `Keyframe`s is the obvious fix and is only HALF right, because reanimated's two
19
+ * web directions differ in one decisive argument:
20
+ *
21
+ * - `exiting` -> `handleExitingAnimation` calls `setElementAnimation` on a
22
+ * throwaway clone with `shouldSavePosition` FALSE. The custom keyframe's
23
+ * `duration x 5` cleanup only tears that clone down, which is what it is for.
24
+ * A `Keyframe` here is safe AND exact: it compiles to a real CSS `@keyframes`
25
+ * rule, so the full scale + opacity shape survives.
26
+ * - `entering` -> `chooseAction` calls `setElementAnimation` on the REAL element
27
+ * with `shouldSavePosition` TRUE, and since a custom keyframe's generated
28
+ * `REA-ENTERING-n` name is absent from reanimated's built-in `Animations`
29
+ * map, `scheduleAnimationCleanup` fires
30
+ * `setElementPosition` at `duration x 5` — `position: absolute`, a frozen
31
+ * `top`/`left`/`width`/`height` snapshot, `transform` cleared. The animation
32
+ * plays and the element is then pinned, silently, one and a half seconds
33
+ * later. That is failure mode (C) in `~/Oxy/AGENTS.md` and it is strictly
34
+ * worse than the inert animation it would replace.
35
+ *
36
+ * So the enter uses a PREDEFINED builder, whose `presetName` IS in `Animations`
37
+ * and therefore skips `scheduleAnimationCleanup` entirely. Predefined builders
38
+ * cannot combine a fade with a scale, so the web enter keeps the fade and drops
39
+ * the 0.7 -> 1 growth. `ScreenTransition` in this same folder already made that
40
+ * trade for the same reason ("on web the slide is intentionally dropped").
41
+ *
42
+ * The imperative shared-value drive (failure mode (A)'s remedy, and what
43
+ * `toast/ToastRow.tsx` uses for its own enter) is the third option and is not
44
+ * available here: these are exported VALUES, not components, so there is no
45
+ * render to hang a `useSharedValue` off — and it could not express an exit at
46
+ * all, since keeping an unmounting element alive is exactly what `exiting` is for.
47
+ *
48
+ * WEB EASING IS LINEAR. `Keyframe` has no `.easing()` modifier, and a per-stop
49
+ * easing only survives `convertAnimationObjectToKeyframes` when it resolves to one
50
+ * of reanimated's seven `WebEasings` names (all of them ease-IN curves), while
51
+ * `.easing(Easing.bezier(...))` needs the `__closure` the worklets babel plugin
52
+ * adds — absent in every Oxy web build — and degrades to linear WITH a warning.
53
+ * `ShrinkAndPop` therefore carries an extra midpoint stop so its piecewise-linear
54
+ * web curve tracks the native `withTiming` segments closely rather than cutting
55
+ * the corner.
56
+ */
57
+ import {
58
+ FadeIn,
59
+ Keyframe,
60
+ type EntryOrExitLayoutType,
61
+ } from 'react-native-reanimated';
62
+
63
+ /** `withTiming`'s default, which is what the native builders animate over. */
64
+ const DEFAULT_DURATION = 300;
65
+ /** `ShrinkAndPop`: 75ms shrink + 150ms overshoot, with the fade ending with it. */
66
+ const POP_DURATION = 250;
67
+
68
+ /**
69
+ * Entering: fade in.
70
+ *
71
+ * The scale is dropped on web — see the header. `FadeIn`'s own default duration
72
+ * is already 300ms, matching the native builder, and the bare class is exported
73
+ * rather than a `.duration()` instance so nothing shares a mutable builder.
74
+ */
75
+ export const ScaleAndFadeIn: EntryOrExitLayoutType = FadeIn;
76
+
77
+ /** Exiting counterpart to {@link ScaleAndFadeIn}: fade out while shrinking to 70%. */
78
+ export const ScaleAndFadeOut: EntryOrExitLayoutType = new Keyframe({
79
+ from: { opacity: 1, transform: [{ scale: 1 }] },
80
+ to: { opacity: 0, transform: [{ scale: 0.7 }] },
81
+ }).duration(DEFAULT_DURATION);
82
+
83
+ /**
84
+ * Exiting with a brief "pop": the scale dips to 70%, overshoots to 110%, and the
85
+ * element fades out.
86
+ *
87
+ * Stops are the native timeline sampled at its own breakpoints, so every property
88
+ * is declared at every stop and nothing depends on CSS's cross-stop interpolation:
89
+ * scale reaches 0.7 at 75ms (30%) and 1.1 at 225ms (90%); opacity holds until
90
+ * 125ms (50%) and then falls to 0 by 250ms. The 50% stop's 0.8333 scale and the
91
+ * 90% stop's 0.2 opacity are the native values at those instants.
92
+ */
93
+ export const ShrinkAndPop: EntryOrExitLayoutType = new Keyframe({
94
+ 0: { opacity: 1, transform: [{ scale: 1 }] },
95
+ 30: { opacity: 1, transform: [{ scale: 0.7 }] },
96
+ 50: { opacity: 1, transform: [{ scale: 0.8333 }] },
97
+ 90: { opacity: 0.2, transform: [{ scale: 1.1 }] },
98
+ 100: { opacity: 0, transform: [{ scale: 1.1 }] },
99
+ }).duration(POP_DURATION);
@@ -2,8 +2,9 @@
2
2
  * Ported from expo-glass-tabs v0.1.1 — src/progressive-blur.tsx
3
3
  * (MIT © 2026 David Mokos). See the top-level NOTICE.
4
4
  */
5
- import { View, type ViewStyle } from 'react-native';
5
+ import { View } from 'react-native';
6
6
 
7
+ import type { WebCssStyle } from '../styles/web-view-style';
7
8
  import { useTheme } from '../theme/use-theme';
8
9
  import {
9
10
  buildMaskGradient,
@@ -50,14 +51,14 @@ export function ProgressiveBlur({
50
51
  // Web-only CSS, unknown to React Native's `ViewStyle`. Both spellings are set
51
52
  // for `backdrop-filter` and `mask-image`: Safari still needs the prefixed
52
53
  // form of each.
53
- const webStyle = {
54
+ const webStyle: WebCssStyle = {
54
55
  pointerEvents: 'none',
55
56
  backdropFilter: filter,
56
57
  WebkitBackdropFilter: filter,
57
58
  maskImage: mask,
58
59
  WebkitMaskImage: mask,
59
60
  backgroundImage: buildTailGradient(colors.background, direction),
60
- } as ViewStyle;
61
+ };
61
62
 
62
63
  return <View {...rest} style={[webStyle, style]} />;
63
64
  }
@@ -19,3 +19,53 @@ import type { ViewStyle } from 'react-native';
19
19
  * where Yoga silently ignores it.
20
20
  */
21
21
  export const WEB_POSITION_FIXED = 'fixed' as ViewStyle['position'];
22
+
23
+ /**
24
+ * A `ViewStyle` that may ALSO carry CSS react-native-web forwards straight to the
25
+ * DOM node but React Native does not model at all.
26
+ *
27
+ * This is the OTHER shape of the same RN/RNW type gap `WEB_POSITION_FIXED`
28
+ * covers, and it needs a different escape hatch because the two are structurally
29
+ * different: `position` is a key RN DOES model, just with a narrower value union,
30
+ * so a value-level cast is enough and it can still be written inside an ordinary
31
+ * style object. The properties below have no key on `ViewStyle` at all, so the
32
+ * whole OBJECT has to cross the boundary.
33
+ *
34
+ * ANNOTATE, DO NOT CAST. Every `.web` fork used to spell this `{ … } as ViewStyle`
35
+ * inline. That is not a widening, it is SILENCING: `as` only requires the two
36
+ * types to be COMPARABLE, so `{ animatoin: '…' } as ViewStyle` compiles happily
37
+ * and the typo ships as a style key react-native-web drops on the floor.
38
+ * Annotating with this interface restores excess-property checking — a key that
39
+ * is neither a real RN style nor a listed web-only property is a compile error —
40
+ * and, unlike a widening helper function, it erases completely, so the emitted
41
+ * JavaScript is byte-identical to the cast it replaces.
42
+ *
43
+ * ```ts
44
+ * const glass: WebCssStyle = { backdropFilter: 'blur(24px)' };
45
+ * // …then pass it anywhere a `ViewStyle` is expected.
46
+ * ```
47
+ *
48
+ * Only add a key here once react-native-web is confirmed to pass it through and
49
+ * RN has no equivalent. Several near-misses do NOT belong: RN 0.83 models
50
+ * `cursor`, `boxShadow`, `filter` and `pointerEvents` on `ViewStyle` directly, so
51
+ * those need no widening at all (`userSelect` is the trap — RN declares it on
52
+ * `TextStyle` only). Prefixed spellings sit alongside the standard ones because
53
+ * Safari still needs them for `backdrop-filter` and `mask-image`.
54
+ */
55
+ export interface WebCssStyle extends ViewStyle {
56
+ /** CSS `animation` shorthand. Bloom's web forks self-inject the `@keyframes`. */
57
+ animation?: string;
58
+ animationDelay?: string;
59
+ transitionProperty?: string;
60
+ transitionDuration?: string;
61
+ transitionTimingFunction?: string;
62
+ backdropFilter?: string;
63
+ WebkitBackdropFilter?: string;
64
+ maskImage?: string;
65
+ WebkitMaskImage?: string;
66
+ backgroundImage?: string;
67
+ scrollbarWidth?: 'auto' | 'thin' | 'none';
68
+ scrollbarColor?: string;
69
+ /** RN declares this on `TextStyle`, not `ViewStyle`. */
70
+ userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all';
71
+ }
@@ -260,16 +260,21 @@ function TabBarBody({
260
260
  [EXPANDED_HEIGHT, MINIMIZED_HEIGHT],
261
261
  Extrapolation.CLAMP,
262
262
  );
263
- return {
264
- height,
265
- // Revolut-style: the pill shrinks in both dimensions.
266
- marginHorizontal: interpolate(
267
- progress.value,
268
- [0, 1],
269
- [0, MINIMIZED_INSET],
270
- Extrapolation.CLAMP,
271
- ),
272
- };
263
+ // Revolut-style: the pill shrinks in both dimensions.
264
+ //
265
+ // Written as the two long-form margins, NEVER as `marginHorizontal`.
266
+ // Reanimated's web path hands a mapper's result to react-native-web's
267
+ // `createReactDOMStyle`, whose `STYLE_SHORT_FORM_EXPANSIONS` map covers
268
+ // `marginInline` but NOT React Native's `marginHorizontal` (verified absent
269
+ // from the whole file in RNW 0.21.2). An unexpanded shorthand is written
270
+ // straight onto the DOM node as `style.marginHorizontal`, which is not a CSS
271
+ // property, so the browser drops it silently: the bar keeps its full width
272
+ // while `highlightStyle` still subtracts `MINIMIZED_INSET` from `barWidth`,
273
+ // leaving the highlight narrower than the tab it sits under. Static styles
274
+ // are unaffected — they take RNW's own StyleSheet path, which does handle
275
+ // the shorthand — so this only bites inside a mapper.
276
+ const inset = interpolate(progress.value, [0, 1], [0, MINIMIZED_INSET], Extrapolation.CLAMP);
277
+ return { height, marginLeft: inset, marginRight: inset };
273
278
  }, [progress]);
274
279
 
275
280
  // The capsule shape lives on the surface itself: iOS 26 glass renders its own
@@ -317,8 +322,8 @@ function TabBarBody({
317
322
  };
318
323
  }, [progress, slideIndex, windowWidth, tabCount]);
319
324
 
320
- // Shared with `useTabBarReservedSpace`, so a consumer reserving layout space
321
- // for the bar can never drift from where the bar actually sits.
325
+ // Shared with `useTabBarFootprint`, so a consumer accounting for the bar in
326
+ // its own layout can never drift from where the bar actually sits.
322
327
  const bottomOffset = tabBarBottomGap(insets.bottom);
323
328
  const barContext = useMemo(
324
329
  () => ({ slideIndex, isDragging, theme, activeIndex, selectIndex }),
@@ -29,5 +29,5 @@ export {
29
29
  useTabBarMinimized,
30
30
  } from './minimize-context';
31
31
  export type { MinimizeState } from './minimize-context';
32
- export { useTabBarReservedSpace } from './use-reserved-space';
32
+ export { useTabBarFootprint } from './use-footprint';
33
33
  export type { TabBarButtonProps, TabBarItem, TabBarProps, TabBarTheme } from './types';
@@ -31,5 +31,5 @@ export {
31
31
  useTabBarMinimized,
32
32
  } from './minimize-context';
33
33
  export type { MinimizeState } from './minimize-context';
34
- export { useTabBarReservedSpace } from './use-reserved-space';
34
+ export { useTabBarFootprint } from './use-footprint';
35
35
  export type { TabBarButtonProps, TabBarItem, TabBarProps, TabBarTheme } from './types';
@@ -63,9 +63,9 @@ const MIN_BOTTOM_GAP = 12;
63
63
  /**
64
64
  * Vertical gap between the bottom of the pill and the bottom of the window.
65
65
  *
66
- * The safe-area inset is folded in HERE, which is why anything reserving layout
67
- * space for the bar must go through `useTabBarReservedSpace` (which derives its
68
- * number from this same function) rather than adding `insets.bottom` to a
66
+ * The safe-area inset is folded in HERE, which is why anything accounting for
67
+ * the bar in its own layout must go through `useTabBarFootprint` (which derives
68
+ * its number from this same function) rather than adding `insets.bottom` to a
69
69
  * hardcoded height — that double-counts the inset.
70
70
  */
71
71
  export function tabBarBottomGap(bottomInset: number): number {
@@ -19,9 +19,10 @@
19
19
  * Browsers without `backdrop-filter` simply skip it and are left with the
20
20
  * translucent tint — a graceful degradation that needs no `@supports` rule.
21
21
  */
22
- import { StyleSheet, type ViewStyle } from 'react-native';
22
+ import { StyleSheet } from 'react-native';
23
23
  import Animated from 'react-native-reanimated';
24
24
 
25
+ import type { WebCssStyle } from '../styles/web-view-style';
25
26
  import type { TabBarSurfaceProps } from './shared';
26
27
 
27
28
  /**
@@ -35,10 +36,10 @@ const GLASS_FILTER = `blur(${GLASS_BLUR_PX}px) saturate(180%)`;
35
36
 
36
37
  // Web-only CSS, unknown to React Native's `ViewStyle`. Both spellings are set:
37
38
  // Safari still needs the prefixed property for `backdrop-filter`.
38
- const glassFilterStyle = {
39
+ const glassFilterStyle: WebCssStyle = {
39
40
  backdropFilter: GLASS_FILTER,
40
41
  WebkitBackdropFilter: GLASS_FILTER,
41
- } as ViewStyle;
42
+ };
42
43
 
43
44
  export function TabBarSurface({ theme, style }: TabBarSurfaceProps) {
44
45
  return (