@oxyhq/bloom 0.51.1 → 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.
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/commonjs/design-tokens/shadows.js +4 -0
- package/lib/commonjs/design-tokens/shadows.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +26 -10
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/frosted-icon-button/shared.js +10 -0
- package/lib/commonjs/frosted-icon-button/shared.js.map +1 -1
- package/lib/commonjs/index.js +66 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +90 -90
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/loading/Loading.web.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.web.js +10 -6
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/commonjs/motion/index.js +1 -1
- package/lib/commonjs/motion/index.web.js +32 -0
- package/lib/commonjs/motion/index.web.js.map +1 -0
- package/lib/commonjs/motion/motion.js +10 -2
- package/lib/commonjs/motion/motion.js.map +1 -1
- package/lib/commonjs/motion/motion.web.js +137 -0
- package/lib/commonjs/motion/motion.web.js.map +1 -0
- package/lib/commonjs/progressive-blur/index.web.js.map +1 -1
- package/lib/commonjs/styles/web-view-style.js +33 -0
- package/lib/commonjs/styles/web-view-style.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +46 -7
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tab-bar/glyph.js +9 -4
- package/lib/commonjs/tab-bar/glyph.js.map +1 -1
- package/lib/commonjs/tab-bar/glyph.native.js +7 -2
- package/lib/commonjs/tab-bar/glyph.native.js.map +1 -1
- package/lib/commonjs/tab-bar/index.js +7 -0
- package/lib/commonjs/tab-bar/index.js.map +1 -1
- package/lib/commonjs/tab-bar/index.web.js +7 -0
- package/lib/commonjs/tab-bar/index.web.js.map +1 -1
- package/lib/commonjs/tab-bar/shared.js +32 -1
- package/lib/commonjs/tab-bar/shared.js.map +1 -1
- package/lib/commonjs/tab-bar/surface.web.js.map +1 -1
- package/lib/commonjs/tab-bar/use-footprint.js +56 -0
- package/lib/commonjs/tab-bar/use-footprint.js.map +1 -0
- package/lib/commonjs/toast/ToastRow.js +7 -3
- package/lib/commonjs/toast/ToastRow.js.map +1 -1
- package/lib/commonjs/toast/ToastSwipeHandler.js +29 -13
- package/lib/commonjs/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/commonjs/toast/constants.js +29 -1
- package/lib/commonjs/toast/constants.js.map +1 -1
- package/lib/commonjs/toast/position-utils.js +9 -4
- package/lib/commonjs/toast/position-utils.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/module/design-tokens/shadows.js +4 -0
- package/lib/module/design-tokens/shadows.js.map +1 -1
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +26 -10
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/frosted-icon-button/shared.js +10 -0
- package/lib/module/frosted-icon-button/shared.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/loading/Loading.web.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.web.js +10 -6
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/module/motion/index.js +1 -1
- package/lib/module/motion/index.js.map +1 -1
- package/lib/module/motion/index.web.js +10 -0
- package/lib/module/motion/index.web.js.map +1 -0
- package/lib/module/motion/motion.js +10 -2
- package/lib/module/motion/motion.js.map +1 -1
- package/lib/module/motion/motion.web.js +133 -0
- package/lib/module/motion/motion.web.js.map +1 -0
- package/lib/module/progressive-blur/index.web.js.map +1 -1
- package/lib/module/styles/web-view-style.js +33 -0
- package/lib/module/styles/web-view-style.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +47 -8
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tab-bar/glyph.js +9 -4
- package/lib/module/tab-bar/glyph.js.map +1 -1
- package/lib/module/tab-bar/glyph.native.js +7 -2
- package/lib/module/tab-bar/glyph.native.js.map +1 -1
- package/lib/module/tab-bar/index.js +1 -0
- package/lib/module/tab-bar/index.js.map +1 -1
- package/lib/module/tab-bar/index.web.js +1 -0
- package/lib/module/tab-bar/index.web.js.map +1 -1
- package/lib/module/tab-bar/shared.js +30 -0
- package/lib/module/tab-bar/shared.js.map +1 -1
- package/lib/module/tab-bar/surface.web.js.map +1 -1
- package/lib/module/tab-bar/use-footprint.js +52 -0
- package/lib/module/tab-bar/use-footprint.js.map +1 -0
- package/lib/module/toast/ToastRow.js +8 -4
- package/lib/module/toast/ToastRow.js.map +1 -1
- package/lib/module/toast/ToastSwipeHandler.js +29 -13
- package/lib/module/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/module/toast/constants.js +28 -1
- package/lib/module/toast/constants.js.map +1 -1
- package/lib/module/toast/position-utils.js +9 -4
- package/lib/module/toast/position-utils.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/frosted-icon-button/shared.d.ts +10 -0
- package/lib/typescript/commonjs/frosted-icon-button/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/index.web.d.ts +9 -0
- package/lib/typescript/commonjs/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/motion.d.ts +14 -6
- package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/motion.web.d.ts +79 -0
- package/lib/typescript/commonjs/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/commonjs/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.native.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/glyph.native.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts +1 -0
- package/lib/typescript/commonjs/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts +1 -0
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/shared.d.ts +25 -0
- package/lib/typescript/commonjs/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/types.d.ts +37 -1
- package/lib/typescript/commonjs/tab-bar/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/commonjs/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/constants.d.ts +26 -8
- package/lib/typescript/commonjs/toast/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/position-utils.d.ts +8 -3
- package/lib/typescript/commonjs/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/frosted-icon-button/shared.d.ts +10 -0
- package/lib/typescript/module/frosted-icon-button/shared.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/module/motion/index.web.d.ts +9 -0
- package/lib/typescript/module/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/module/motion/motion.d.ts +14 -6
- package/lib/typescript/module/motion/motion.d.ts.map +1 -1
- package/lib/typescript/module/motion/motion.web.d.ts +79 -0
- package/lib/typescript/module/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/module/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/module/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/glyph.d.ts +1 -1
- package/lib/typescript/module/tab-bar/glyph.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/glyph.native.d.ts +1 -1
- package/lib/typescript/module/tab-bar/glyph.native.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts +1 -0
- package/lib/typescript/module/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts +1 -0
- package/lib/typescript/module/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/shared.d.ts +25 -0
- package/lib/typescript/module/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/types.d.ts +37 -1
- package/lib/typescript/module/tab-bar/types.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/module/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/module/toast/constants.d.ts +26 -8
- package/lib/typescript/module/toast/constants.d.ts.map +1 -1
- package/lib/typescript/module/toast/position-utils.d.ts +8 -3
- package/lib/typescript/module/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/Motion.test.tsx +31 -3
- package/src/__tests__/MotionWebFork.test.ts +140 -0
- package/src/__tests__/TabBar.test.tsx +82 -2
- package/src/__tests__/TabBarAnimatedShorthand.test.ts +135 -0
- package/src/__tests__/TabBarFootprint.test.tsx +172 -0
- package/src/__tests__/TabBarLongPress.test.tsx +258 -0
- package/src/__tests__/TabBarWebFork.test.ts +1 -0
- package/src/__tests__/ToastHostWebFork.test.ts +6 -2
- package/src/__tests__/Toaster.test.tsx +62 -1
- package/src/__tests__/toast-position-utils.test.ts +23 -7
- package/src/__tests__/web-css-style.test.ts +139 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +11 -7
- package/src/connection-dots/ConnectionDots.web.tsx +4 -3
- package/src/design-tokens/shadows.ts +5 -1
- package/src/dialog/Dialog.tsx +1 -1
- package/src/dialog/Dialog.web.tsx +41 -26
- package/src/frosted-icon-button/shared.ts +10 -0
- package/src/index.web.ts +2 -2
- package/src/loading/Loading.web.tsx +7 -5
- package/src/loading/SpinnerIcon.web.tsx +12 -9
- package/src/motion/index.web.ts +8 -0
- package/src/motion/motion.ts +14 -6
- package/src/motion/motion.web.ts +99 -0
- package/src/progressive-blur/index.web.tsx +4 -3
- package/src/styles/web-view-style.ts +50 -0
- package/src/tab-bar/TabBarBase.tsx +54 -15
- package/src/tab-bar/glyph.native.tsx +5 -2
- package/src/tab-bar/glyph.tsx +8 -4
- package/src/tab-bar/index.ts +1 -0
- package/src/tab-bar/index.web.tsx +1 -0
- package/src/tab-bar/shared.ts +38 -0
- package/src/tab-bar/surface.web.tsx +4 -3
- package/src/tab-bar/types.ts +37 -1
- package/src/tab-bar/use-footprint.ts +50 -0
- package/src/toast/ToastRow.tsx +8 -3
- package/src/toast/ToastSwipeHandler.tsx +30 -12
- package/src/toast/constants.ts +28 -0
- package/src/toast/position-utils.ts +10 -5
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +9 -8
|
@@ -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<
|
|
629
|
-
() =>
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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<
|
|
639
|
-
() =>
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
881
|
+
},
|
|
867
882
|
};
|
|
868
883
|
|
|
869
884
|
/**
|
|
@@ -109,6 +109,16 @@ type FillableElement = ReactElement<{ fill?: unknown }>;
|
|
|
109
109
|
* so any bare Bloom icon element gets the theme-aware color for free while an
|
|
110
110
|
* explicitly-colored icon is never overridden. Non-element children (plain
|
|
111
111
|
* text/strings) pass through unchanged.
|
|
112
|
+
*
|
|
113
|
+
* KNOWN LIMIT, deliberately not papered over: an icon component that paints from
|
|
114
|
+
* a `color` PROP rather than from `fill`/`style.color` ignores this entirely, and
|
|
115
|
+
* does so SILENTLY — nothing throws, the glyph renders, it simply never takes the
|
|
116
|
+
* tint. `color` is not injected alongside `fill` because the prop is not ours to
|
|
117
|
+
* claim: components use it for semantic variants (`color="danger"`), so writing an
|
|
118
|
+
* `rgb(…)` string into it can produce a worse failure than the one it fixes, and
|
|
119
|
+
* an icon already carrying its own `color` would be clobbered. Consumers with such
|
|
120
|
+
* a set pre-color the element themselves; the tab bar additionally offers
|
|
121
|
+
* `TabBarItem.activeIcon`, which carries selection by shape instead of by tint.
|
|
112
122
|
*/
|
|
113
123
|
export function applyIconColor(node: ReactNode, color: string): ReactNode {
|
|
114
124
|
if (!isValidElement(node)) return node;
|
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
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
|
|
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';
|
package/src/motion/motion.ts
CHANGED
|
@@ -2,17 +2,25 @@ import {
|
|
|
2
2
|
withDelay,
|
|
3
3
|
withSequence,
|
|
4
4
|
withTiming,
|
|
5
|
-
type
|
|
5
|
+
type EntryOrExitLayoutType,
|
|
6
6
|
} from 'react-native-reanimated';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Reanimated
|
|
10
|
-
*
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
}
|
|
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
|
+
}
|
|
@@ -49,10 +49,12 @@ import {
|
|
|
49
49
|
ITEM_GAP,
|
|
50
50
|
ITEM_PAD_V,
|
|
51
51
|
LABEL_FONT_SIZE,
|
|
52
|
+
LONG_PRESS_MIN_DURATION,
|
|
52
53
|
MINIMIZED_HEIGHT,
|
|
53
54
|
MINIMIZED_INSET,
|
|
54
55
|
ROW_PAD_H,
|
|
55
56
|
SLIDE_SPRING,
|
|
57
|
+
tabBarBottomGap,
|
|
56
58
|
useTabBarTheme,
|
|
57
59
|
type TabBarGlyphProps,
|
|
58
60
|
type TabBarSurfaceProps,
|
|
@@ -93,6 +95,7 @@ function TabBarBody({
|
|
|
93
95
|
children,
|
|
94
96
|
activeIndex,
|
|
95
97
|
onIndexChange,
|
|
98
|
+
onIndexLongPress,
|
|
96
99
|
theme: themeOverrides,
|
|
97
100
|
haptics = true,
|
|
98
101
|
style,
|
|
@@ -121,6 +124,14 @@ function TabBarBody({
|
|
|
121
124
|
// scrubbing makes the content jump under the finger.
|
|
122
125
|
const selectIndex = useCallback((index: number) => onIndexChange?.(index), [onIndexChange]);
|
|
123
126
|
|
|
127
|
+
// Long press is reported through the same shape, and deliberately does NOT
|
|
128
|
+
// select: it is a secondary action on the tab under the finger.
|
|
129
|
+
const longPressIndex = useCallback(
|
|
130
|
+
(index: number) => onIndexLongPress?.(index),
|
|
131
|
+
[onIndexLongPress],
|
|
132
|
+
);
|
|
133
|
+
const hasLongPress = onIndexLongPress !== undefined;
|
|
134
|
+
|
|
124
135
|
// CONTROLLED path: the bar owns the highlight when `activeIndex` is provided,
|
|
125
136
|
// so it tracks the prop even when a consumer renders custom children instead
|
|
126
137
|
// of `TabBarButton`s. Syncing an imperative animation to a controlled prop is
|
|
@@ -200,11 +211,30 @@ function TabBarBody({
|
|
|
200
211
|
runOnJS(selectIndex)(index);
|
|
201
212
|
});
|
|
202
213
|
|
|
203
|
-
return Gesture.Race(pan, tap);
|
|
214
|
+
if (!hasLongPress) return Gesture.Race(pan, tap);
|
|
215
|
+
|
|
216
|
+
// Long press: the ONLY path to one, because the detector consumes the
|
|
217
|
+
// bar's touches and a button's inner `Pressable` therefore never sees a
|
|
218
|
+
// real finger (see `TabBarButtonBody`). Added to the race only when a
|
|
219
|
+
// handler was supplied — an always-armed long press would cancel the pan
|
|
220
|
+
// for anyone who rests a finger before scrubbing, and no existing consumer
|
|
221
|
+
// asked for that. It cannot cost a tap either: the tap gesture gives up at
|
|
222
|
+
// 400ms, before this one can activate at 500ms, so the two are never
|
|
223
|
+
// candidates at the same instant. Resolves its index through the same
|
|
224
|
+
// `indexAtX` worklet as tap and pan, so all three agree on the geometry.
|
|
225
|
+
const longPress = Gesture.LongPress()
|
|
226
|
+
.minDuration(LONG_PRESS_MIN_DURATION)
|
|
227
|
+
.onStart((event) => {
|
|
228
|
+
runOnJS(longPressIndex)(Math.round(indexAtX(event.x, progress.value)));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return Gesture.Race(pan, tap, longPress);
|
|
204
232
|
}, [
|
|
205
233
|
windowWidth,
|
|
206
234
|
tabCount,
|
|
207
235
|
selectIndex,
|
|
236
|
+
hasLongPress,
|
|
237
|
+
longPressIndex,
|
|
208
238
|
tick,
|
|
209
239
|
isDragging,
|
|
210
240
|
lastTicked,
|
|
@@ -230,16 +260,21 @@ function TabBarBody({
|
|
|
230
260
|
[EXPANDED_HEIGHT, MINIMIZED_HEIGHT],
|
|
231
261
|
Extrapolation.CLAMP,
|
|
232
262
|
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 };
|
|
243
278
|
}, [progress]);
|
|
244
279
|
|
|
245
280
|
// The capsule shape lives on the surface itself: iOS 26 glass renders its own
|
|
@@ -287,7 +322,9 @@ function TabBarBody({
|
|
|
287
322
|
};
|
|
288
323
|
}, [progress, slideIndex, windowWidth, tabCount]);
|
|
289
324
|
|
|
290
|
-
|
|
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.
|
|
327
|
+
const bottomOffset = tabBarBottomGap(insets.bottom);
|
|
291
328
|
const barContext = useMemo(
|
|
292
329
|
() => ({ slideIndex, isDragging, theme, activeIndex, selectIndex }),
|
|
293
330
|
[slideIndex, isDragging, theme, activeIndex, selectIndex],
|
|
@@ -430,11 +467,13 @@ function TabBarButtonBody({
|
|
|
430
467
|
}
|
|
431
468
|
>
|
|
432
469
|
<Animated.View style={[styles.itemBox, boxStyle]}>
|
|
433
|
-
{/* Inactive glyph underneath, active glyph crossfading on top.
|
|
470
|
+
{/* Inactive glyph underneath, active glyph crossfading on top. `active`
|
|
471
|
+
tells each layer which one it is, so an item with an `activeIcon`
|
|
472
|
+
crossfades between two different nodes and not just two tints. */}
|
|
434
473
|
<View>
|
|
435
|
-
<Glyph item={item} tint={theme.inactiveTint} size={ICON_SIZE} />
|
|
474
|
+
<Glyph item={item} tint={theme.inactiveTint} size={ICON_SIZE} active={false} />
|
|
436
475
|
<Animated.View style={[StyleSheet.absoluteFill, styles.glyphLayer, activeGlyphStyle]}>
|
|
437
|
-
<Glyph item={item} tint={theme.activeTint} size={ICON_SIZE} />
|
|
476
|
+
<Glyph item={item} tint={theme.activeTint} size={ICON_SIZE} active />
|
|
438
477
|
</Animated.View>
|
|
439
478
|
</View>
|
|
440
479
|
{/* Fades out and is clipped by the shrinking box — no layout anim. */}
|
|
@@ -17,7 +17,7 @@ import { Platform, View } from 'react-native';
|
|
|
17
17
|
import { applyIconColor } from '../frosted-icon-button/shared';
|
|
18
18
|
import type { TabBarGlyphProps } from './shared';
|
|
19
19
|
|
|
20
|
-
export function TabBarGlyph({ item, tint, size }: TabBarGlyphProps) {
|
|
20
|
+
export function TabBarGlyph({ item, tint, size, active }: TabBarGlyphProps) {
|
|
21
21
|
if (item.sfSymbol && Platform.OS === 'ios') {
|
|
22
22
|
// `TabBarItem.sfSymbol` is a plain `string` because `types.ts` may import
|
|
23
23
|
// from react/react-native only — pulling `expo-symbols`' `SFSymbol` union
|
|
@@ -36,9 +36,12 @@ export function TabBarGlyph({ item, tint, size }: TabBarGlyphProps) {
|
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Same node-swapping crossfade as the neutral variant: the active layer takes
|
|
40
|
+
// `activeIcon` when the item has one. An SF Symbol never reaches here, and is
|
|
41
|
+
// tinted natively above, so the tint crossfade remains its correct expression.
|
|
39
42
|
return (
|
|
40
43
|
<View style={{ height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
41
|
-
{applyIconColor(item.icon, tint)}
|
|
44
|
+
{applyIconColor(active ? (item.activeIcon ?? item.icon) : item.icon, tint)}
|
|
42
45
|
</View>
|
|
43
46
|
);
|
|
44
47
|
}
|