@oxyhq/bloom 0.26.1 → 0.28.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.
- package/lib/commonjs/animated-check/AnimatedCheck.js +162 -0
- package/lib/commonjs/animated-check/AnimatedCheck.js.map +1 -0
- package/lib/commonjs/animated-check/index.js +13 -0
- package/lib/commonjs/animated-check/index.js.map +1 -0
- package/lib/commonjs/avatar/Avatar.js +67 -96
- package/lib/commonjs/avatar/Avatar.js.map +1 -1
- package/lib/commonjs/avatar/AvatarRing.js +183 -0
- package/lib/commonjs/avatar/AvatarRing.js.map +1 -0
- package/lib/commonjs/avatar/index.js.map +1 -1
- package/lib/commonjs/avatar/squircle-path.js +14 -0
- package/lib/commonjs/avatar/squircle-path.js.map +1 -0
- package/lib/commonjs/avatar/svg-module.js +15 -0
- package/lib/commonjs/avatar/svg-module.js.map +1 -0
- package/lib/commonjs/dialog/placement.js +9 -7
- package/lib/commonjs/dialog/placement.js.map +1 -1
- package/lib/commonjs/fill/index.js.map +1 -1
- package/lib/commonjs/hooks/index.js +20 -0
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/useGutters.js +106 -0
- package/lib/commonjs/hooks/useGutters.js.map +1 -0
- package/lib/commonjs/hooks/useHaptics.js +69 -0
- package/lib/commonjs/hooks/useHaptics.js.map +1 -0
- package/lib/commonjs/hooks/useInteractionState.js +8 -2
- package/lib/commonjs/hooks/useInteractionState.js.map +1 -1
- package/lib/commonjs/index.js +155 -63
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +155 -63
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/media-inset-border/MediaInsetBorder.js +50 -0
- package/lib/commonjs/media-inset-border/MediaInsetBorder.js.map +1 -0
- package/lib/commonjs/media-inset-border/index.js +13 -0
- package/lib/commonjs/media-inset-border/index.js.map +1 -0
- package/lib/commonjs/motion/ScreenTransition.js +46 -0
- package/lib/commonjs/motion/ScreenTransition.js.map +1 -0
- package/lib/commonjs/motion/index.js +32 -0
- package/lib/commonjs/motion/index.js.map +1 -0
- package/lib/commonjs/motion/motion.js +93 -0
- package/lib/commonjs/motion/motion.js.map +1 -0
- package/lib/commonjs/pressable-scale/PressableScale.js +75 -0
- package/lib/commonjs/pressable-scale/PressableScale.js.map +1 -0
- package/lib/commonjs/pressable-scale/index.js +13 -0
- package/lib/commonjs/pressable-scale/index.js.map +1 -0
- package/lib/commonjs/pressable-with-hover/PressableWithHover.js +36 -0
- package/lib/commonjs/pressable-with-hover/PressableWithHover.js.map +1 -0
- package/lib/commonjs/pressable-with-hover/index.js +13 -0
- package/lib/commonjs/pressable-with-hover/index.js.map +1 -0
- package/lib/commonjs/styles/breakpoints.js +24 -0
- package/lib/commonjs/styles/breakpoints.js.map +1 -0
- package/lib/commonjs/styles/index.js +7 -0
- package/lib/commonjs/styles/index.js.map +1 -1
- package/lib/commonjs/subtle-hover/SubtleHover.js +69 -0
- package/lib/commonjs/subtle-hover/SubtleHover.js.map +1 -0
- package/lib/commonjs/subtle-hover/index.js +13 -0
- package/lib/commonjs/subtle-hover/index.js.map +1 -0
- package/lib/commonjs/theme/build-theme.js +2 -0
- package/lib/commonjs/theme/build-theme.js.map +1 -1
- package/lib/commonjs/theme/gradients.js +39 -0
- package/lib/commonjs/theme/gradients.js.map +1 -0
- package/lib/commonjs/theme/index.js +7 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/module/animated-check/AnimatedCheck.js +157 -0
- package/lib/module/animated-check/AnimatedCheck.js.map +1 -0
- package/lib/module/animated-check/index.js +4 -0
- package/lib/module/animated-check/index.js.map +1 -0
- package/lib/module/avatar/Avatar.js +66 -96
- package/lib/module/avatar/Avatar.js.map +1 -1
- package/lib/module/avatar/AvatarRing.js +177 -0
- package/lib/module/avatar/AvatarRing.js.map +1 -0
- package/lib/module/avatar/index.js.map +1 -1
- package/lib/module/avatar/squircle-path.js +10 -0
- package/lib/module/avatar/squircle-path.js.map +1 -0
- package/lib/module/avatar/svg-module.js +11 -0
- package/lib/module/avatar/svg-module.js.map +1 -0
- package/lib/module/dialog/placement.js +9 -7
- package/lib/module/dialog/placement.js.map +1 -1
- package/lib/module/fill/index.js.map +1 -1
- package/lib/module/hooks/index.js +2 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useGutters.js +103 -0
- package/lib/module/hooks/useGutters.js.map +1 -0
- package/lib/module/hooks/useHaptics.js +65 -0
- package/lib/module/hooks/useHaptics.js.map +1 -0
- package/lib/module/hooks/useInteractionState.js +10 -3
- package/lib/module/hooks/useInteractionState.js.map +1 -1
- package/lib/module/index.js +10 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +10 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/media-inset-border/MediaInsetBorder.js +47 -0
- package/lib/module/media-inset-border/MediaInsetBorder.js.map +1 -0
- package/lib/module/media-inset-border/index.js +4 -0
- package/lib/module/media-inset-border/index.js.map +1 -0
- package/lib/module/motion/ScreenTransition.js +40 -0
- package/lib/module/motion/ScreenTransition.js.map +1 -0
- package/lib/module/motion/index.js +5 -0
- package/lib/module/motion/index.js.map +1 -0
- package/lib/module/motion/motion.js +87 -0
- package/lib/module/motion/motion.js.map +1 -0
- package/lib/module/pressable-scale/PressableScale.js +72 -0
- package/lib/module/pressable-scale/PressableScale.js.map +1 -0
- package/lib/module/pressable-scale/index.js +4 -0
- package/lib/module/pressable-scale/index.js.map +1 -0
- package/lib/module/pressable-with-hover/PressableWithHover.js +31 -0
- package/lib/module/pressable-with-hover/PressableWithHover.js.map +1 -0
- package/lib/module/pressable-with-hover/index.js +4 -0
- package/lib/module/pressable-with-hover/index.js.map +1 -0
- package/lib/module/styles/breakpoints.js +20 -0
- package/lib/module/styles/breakpoints.js.map +1 -0
- package/lib/module/styles/index.js +1 -0
- package/lib/module/styles/index.js.map +1 -1
- package/lib/module/subtle-hover/SubtleHover.js +66 -0
- package/lib/module/subtle-hover/SubtleHover.js.map +1 -0
- package/lib/module/subtle-hover/index.js +4 -0
- package/lib/module/subtle-hover/index.js.map +1 -0
- package/lib/module/theme/build-theme.js +2 -0
- package/lib/module/theme/build-theme.js.map +1 -1
- package/lib/module/theme/gradients.js +35 -0
- package/lib/module/theme/gradients.js.map +1 -0
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts +18 -0
- package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts.map +1 -0
- package/lib/typescript/commonjs/animated-check/index.d.ts +3 -0
- package/lib/typescript/commonjs/animated-check/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/index.d.ts +1 -1
- package/lib/typescript/commonjs/avatar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/types.d.ts +30 -0
- package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/placement.d.ts +8 -7
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/commonjs/fill/index.d.ts +2 -2
- package/lib/typescript/commonjs/fill/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/useGutters.d.ts +32 -0
- package/lib/typescript/commonjs/hooks/useGutters.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useHaptics.d.ts +28 -0
- package/lib/typescript/commonjs/hooks/useHaptics.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useInteractionState.d.ts +6 -0
- package/lib/typescript/commonjs/hooks/useInteractionState.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +16 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +16 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts +26 -0
- package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-inset-border/index.d.ts +3 -0
- package/lib/typescript/commonjs/media-inset-border/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/ScreenTransition.d.ts +33 -0
- package/lib/typescript/commonjs/motion/ScreenTransition.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/index.d.ts +4 -0
- package/lib/typescript/commonjs/motion/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/motion.d.ts +24 -0
- package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts +21 -0
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-scale/index.d.ts +3 -0
- package/lib/typescript/commonjs/pressable-scale/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts +18 -0
- package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
- package/lib/typescript/commonjs/pressable-with-hover/index.d.ts +3 -0
- package/lib/typescript/commonjs/pressable-with-hover/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/styles/breakpoints.d.ts +18 -0
- package/lib/typescript/commonjs/styles/breakpoints.d.ts.map +1 -0
- package/lib/typescript/commonjs/styles/index.d.ts +2 -0
- package/lib/typescript/commonjs/styles/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts +32 -0
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts.map +1 -0
- package/lib/typescript/commonjs/subtle-hover/index.d.ts +3 -0
- package/lib/typescript/commonjs/subtle-hover/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/gradients.d.ts +12 -0
- package/lib/typescript/commonjs/theme/gradients.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/index.d.ts +2 -1
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/types.d.ts +11 -0
- package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
- package/lib/typescript/module/animated-check/AnimatedCheck.d.ts +18 -0
- package/lib/typescript/module/animated-check/AnimatedCheck.d.ts.map +1 -0
- package/lib/typescript/module/animated-check/index.d.ts +3 -0
- package/lib/typescript/module/animated-check/index.d.ts.map +1 -0
- package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/module/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/module/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/module/avatar/index.d.ts +1 -1
- package/lib/typescript/module/avatar/index.d.ts.map +1 -1
- package/lib/typescript/module/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/module/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/module/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/module/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/module/avatar/types.d.ts +30 -0
- package/lib/typescript/module/avatar/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/placement.d.ts +8 -7
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/module/fill/index.d.ts +2 -2
- package/lib/typescript/module/fill/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/index.d.ts +4 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/useGutters.d.ts +32 -0
- package/lib/typescript/module/hooks/useGutters.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useHaptics.d.ts +28 -0
- package/lib/typescript/module/hooks/useHaptics.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useInteractionState.d.ts +6 -0
- package/lib/typescript/module/hooks/useInteractionState.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +16 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +16 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts +26 -0
- package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
- package/lib/typescript/module/media-inset-border/index.d.ts +3 -0
- package/lib/typescript/module/media-inset-border/index.d.ts.map +1 -0
- package/lib/typescript/module/motion/ScreenTransition.d.ts +33 -0
- package/lib/typescript/module/motion/ScreenTransition.d.ts.map +1 -0
- package/lib/typescript/module/motion/index.d.ts +4 -0
- package/lib/typescript/module/motion/index.d.ts.map +1 -0
- package/lib/typescript/module/motion/motion.d.ts +24 -0
- package/lib/typescript/module/motion/motion.d.ts.map +1 -0
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts +21 -0
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts.map +1 -0
- package/lib/typescript/module/pressable-scale/index.d.ts +3 -0
- package/lib/typescript/module/pressable-scale/index.d.ts.map +1 -0
- package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts +18 -0
- package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
- package/lib/typescript/module/pressable-with-hover/index.d.ts +3 -0
- package/lib/typescript/module/pressable-with-hover/index.d.ts.map +1 -0
- package/lib/typescript/module/styles/breakpoints.d.ts +18 -0
- package/lib/typescript/module/styles/breakpoints.d.ts.map +1 -0
- package/lib/typescript/module/styles/index.d.ts +2 -0
- package/lib/typescript/module/styles/index.d.ts.map +1 -1
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts +32 -0
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts.map +1 -0
- package/lib/typescript/module/subtle-hover/index.d.ts +3 -0
- package/lib/typescript/module/subtle-hover/index.d.ts.map +1 -0
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/module/theme/gradients.d.ts +12 -0
- package/lib/typescript/module/theme/gradients.d.ts.map +1 -0
- package/lib/typescript/module/theme/index.d.ts +2 -1
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/types.d.ts +11 -0
- package/lib/typescript/module/theme/types.d.ts.map +1 -1
- package/package.json +73 -1
- package/src/__tests__/AnimatedCheck.test.tsx +31 -0
- package/src/__tests__/MediaInsetBorder.test.tsx +36 -0
- package/src/__tests__/Motion.test.tsx +58 -0
- package/src/__tests__/PressableScale.test.tsx +29 -0
- package/src/__tests__/PressableWithHover.test.tsx +48 -0
- package/src/__tests__/SubtleHover.test.tsx +42 -0
- package/src/__tests__/theme-gradients.test.ts +31 -0
- package/src/__tests__/theme.test.ts +3 -0
- package/src/__tests__/useGutters.test.ts +58 -0
- package/src/__tests__/useHaptics.test.tsx +58 -0
- package/src/__tests__/useInteractionState.test.ts +27 -0
- package/src/animated-check/AnimatedCheck.stories.tsx +60 -0
- package/src/animated-check/AnimatedCheck.tsx +173 -0
- package/src/animated-check/index.ts +2 -0
- package/src/avatar/Avatar.stories.tsx +95 -0
- package/src/avatar/Avatar.tsx +61 -110
- package/src/avatar/AvatarRing.tsx +170 -0
- package/src/avatar/index.ts +6 -1
- package/src/avatar/squircle-path.ts +11 -0
- package/src/avatar/svg-module.ts +11 -0
- package/src/avatar/types.ts +32 -0
- package/src/dialog/placement.ts +10 -7
- package/src/fill/index.tsx +2 -2
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useGutters.ts +99 -0
- package/src/hooks/useHaptics.tsx +69 -0
- package/src/hooks/useInteractionState.ts +8 -2
- package/src/index.ts +20 -0
- package/src/index.web.ts +20 -0
- package/src/media-inset-border/MediaInsetBorder.stories.tsx +60 -0
- package/src/media-inset-border/MediaInsetBorder.tsx +72 -0
- package/src/media-inset-border/index.ts +2 -0
- package/src/motion/Motion.stories.tsx +99 -0
- package/src/motion/ScreenTransition.tsx +71 -0
- package/src/motion/index.ts +3 -0
- package/src/motion/motion.ts +75 -0
- package/src/pressable-scale/PressableScale.stories.tsx +71 -0
- package/src/pressable-scale/PressableScale.tsx +103 -0
- package/src/pressable-scale/index.ts +2 -0
- package/src/pressable-with-hover/PressableWithHover.stories.tsx +43 -0
- package/src/pressable-with-hover/PressableWithHover.tsx +43 -0
- package/src/pressable-with-hover/index.ts +2 -0
- package/src/styles/breakpoints.ts +18 -0
- package/src/styles/index.ts +2 -0
- package/src/subtle-hover/SubtleHover.stories.tsx +72 -0
- package/src/subtle-hover/SubtleHover.tsx +90 -0
- package/src/subtle-hover/index.ts +2 -0
- package/src/theme/build-theme.ts +2 -0
- package/src/theme/gradients.ts +61 -0
- package/src/theme/index.ts +2 -1
- package/src/theme/types.ts +13 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
5
|
+
import { space } from '../styles/tokens';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A named gutter size, or `0` for no padding on that edge.
|
|
9
|
+
*
|
|
10
|
+
* - `'compact'` — tight gutter (dense lists, chips).
|
|
11
|
+
* - `'base'` — the default page gutter.
|
|
12
|
+
* - `'wide'` — roomy gutter (marketing / hero surfaces).
|
|
13
|
+
*/
|
|
14
|
+
export type Gutter = 'compact' | 'base' | 'wide' | 0;
|
|
15
|
+
|
|
16
|
+
/** Resolved edge padding in px — one value per side, ready to spread onto a style. */
|
|
17
|
+
export interface Gutters {
|
|
18
|
+
paddingTop: number;
|
|
19
|
+
paddingRight: number;
|
|
20
|
+
paddingBottom: number;
|
|
21
|
+
paddingLeft: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The active viewport tier: the widest matched breakpoint, or `'base'` below `sm`. */
|
|
25
|
+
type GutterTier = 'base' | 'sm' | 'md' | 'lg' | 'xl';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-size gutter padding (px) at each viewport tier. Padding steps up around
|
|
29
|
+
* tablet width (`md`), mirroring the design-system scale: phones get a tighter
|
|
30
|
+
* gutter, wider screens a roomier one. Values come from the shared `space`
|
|
31
|
+
* tokens so gutters stay on the spacing grid.
|
|
32
|
+
*/
|
|
33
|
+
const GUTTER_SCALE: Record<Exclude<Gutter, 0>, Record<GutterTier, number>> = {
|
|
34
|
+
compact: { base: space.sm, sm: space.sm, md: space.md, lg: space.md, xl: space.md },
|
|
35
|
+
base: { base: space.lg, sm: space.lg, md: space.xl, lg: space.xl, xl: space.xl },
|
|
36
|
+
wide: { base: space.xl, sm: space.xl, md: space._3xl, lg: space._3xl, xl: space._3xl },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Widest-first breakpoint table so the first match wins. */
|
|
40
|
+
const TIERS: ReadonlyArray<readonly [Exclude<GutterTier, 'base'>, number]> = [
|
|
41
|
+
['xl', BREAKPOINTS.xl],
|
|
42
|
+
['lg', BREAKPOINTS.lg],
|
|
43
|
+
['md', BREAKPOINTS.md],
|
|
44
|
+
['sm', BREAKPOINTS.sm],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function resolveTier(width: number): GutterTier {
|
|
48
|
+
for (const [tier, minWidth] of TIERS) {
|
|
49
|
+
if (width >= minWidth) return tier;
|
|
50
|
+
}
|
|
51
|
+
return 'base';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveEdge(size: Gutter, tier: GutterTier): number {
|
|
55
|
+
return size === 0 ? 0 : GUTTER_SCALE[size][tier];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expand a 1/2/4-arg CSS-shorthand gutter list into `[top, right, bottom, left]`.
|
|
60
|
+
* `??` (not `||`) so an explicit `0` is preserved rather than falling through.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeShorthand(gutters: Gutter[]): [Gutter, Gutter, Gutter, Gutter] {
|
|
63
|
+
const top = gutters[0] ?? 0;
|
|
64
|
+
const right = gutters[1] ?? top;
|
|
65
|
+
const bottom = gutters[2] ?? top;
|
|
66
|
+
const left = gutters[3] ?? right;
|
|
67
|
+
return [top, right, bottom, left];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Breakpoint-aware gutter padding. Give it CSS-shorthand gutter sizes and it
|
|
72
|
+
* returns resolved `padding*` values for the current viewport width, recomputed
|
|
73
|
+
* as the window crosses a breakpoint.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const gutters = useGutters(['base']); // uniform
|
|
78
|
+
* const gutters = useGutters([0, 'wide']); // no vertical, wide horizontal
|
|
79
|
+
* const gutters = useGutters(['compact', 'base', 0, 'base']); // t / r / b / l
|
|
80
|
+
* <View style={gutters} />
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function useGutters(gutters: [Gutter]): Gutters;
|
|
84
|
+
export function useGutters(gutters: [Gutter, Gutter]): Gutters;
|
|
85
|
+
export function useGutters(gutters: [Gutter, Gutter, Gutter, Gutter]): Gutters;
|
|
86
|
+
export function useGutters(gutters: Gutter[]): Gutters {
|
|
87
|
+
const { width } = useWindowDimensions();
|
|
88
|
+
const [top, right, bottom, left] = normalizeShorthand(gutters);
|
|
89
|
+
|
|
90
|
+
return useMemo<Gutters>(() => {
|
|
91
|
+
const tier = resolveTier(width);
|
|
92
|
+
return {
|
|
93
|
+
paddingTop: resolveEdge(top, tier),
|
|
94
|
+
paddingRight: resolveEdge(right, tier),
|
|
95
|
+
paddingBottom: resolveEdge(bottom, tier),
|
|
96
|
+
paddingLeft: resolveEdge(left, tier),
|
|
97
|
+
};
|
|
98
|
+
}, [width, top, right, bottom, left]);
|
|
99
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { lazyRequire } from '../utils/lazy-require';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lazily loads `expo-haptics`, returning `null` when it is not installed. The
|
|
8
|
+
* dependency is an optional peer, so calls degrade to a no-op when absent.
|
|
9
|
+
*/
|
|
10
|
+
const getHapticsModule = lazyRequire<typeof import('expo-haptics')>('expo-haptics');
|
|
11
|
+
|
|
12
|
+
/** Impact strength requested by a caller. */
|
|
13
|
+
export type HapticStrength = 'light' | 'medium' | 'heavy';
|
|
14
|
+
|
|
15
|
+
interface BloomHapticsContextValue {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BloomHapticsContext = createContext<BloomHapticsContextValue>({ enabled: true });
|
|
20
|
+
|
|
21
|
+
export interface BloomHapticsProviderProps {
|
|
22
|
+
/** Globally enable or disable haptics for the subtree. Defaults to `true`. */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional provider that toggles haptic feedback for its subtree — e.g. to honour
|
|
29
|
+
* a user "reduce haptics" preference. `useHaptics` defaults to enabled when no
|
|
30
|
+
* provider is present.
|
|
31
|
+
*/
|
|
32
|
+
export function BloomHapticsProvider({ enabled = true, children }: BloomHapticsProviderProps) {
|
|
33
|
+
const value = useMemo(() => ({ enabled }), [enabled]);
|
|
34
|
+
return <BloomHapticsContext.Provider value={value}>{children}</BloomHapticsContext.Provider>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BloomHapticsProvider.displayName = 'BloomHapticsProvider';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a function that fires a haptic impact. The returned callback no-ops on
|
|
41
|
+
* web, when haptics are disabled via {@link BloomHapticsProvider}, or when the
|
|
42
|
+
* optional `expo-haptics` peer is not installed.
|
|
43
|
+
*
|
|
44
|
+
* `strength` defaults to `'light'`. Android is clamped to the light impact style
|
|
45
|
+
* regardless of `strength` — its medium/heavy motors read as too strong for UI
|
|
46
|
+
* feedback (tuned to match iOS perceived intensity).
|
|
47
|
+
*/
|
|
48
|
+
export function useHaptics(): (strength?: HapticStrength) => void {
|
|
49
|
+
const { enabled } = useContext(BloomHapticsContext);
|
|
50
|
+
|
|
51
|
+
return useCallback(
|
|
52
|
+
(strength: HapticStrength = 'light') => {
|
|
53
|
+
if (!enabled || Platform.OS === 'web') return;
|
|
54
|
+
|
|
55
|
+
const haptics = getHapticsModule();
|
|
56
|
+
if (!haptics) return;
|
|
57
|
+
|
|
58
|
+
const { ImpactFeedbackStyle } = haptics;
|
|
59
|
+
let style = ImpactFeedbackStyle.Light;
|
|
60
|
+
if (Platform.OS !== 'android') {
|
|
61
|
+
if (strength === 'medium') style = ImpactFeedbackStyle.Medium;
|
|
62
|
+
else if (strength === 'heavy') style = ImpactFeedbackStyle.Heavy;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void haptics.impactAsync(style);
|
|
66
|
+
},
|
|
67
|
+
[enabled],
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Tracks a single boolean interaction flag (hovered / focused / pressed).
|
|
5
|
+
*
|
|
6
|
+
* Returns a memoized `{ state, onIn, onOut }` with stable handler identities,
|
|
7
|
+
* suitable for spreading onto Pressable-like components.
|
|
8
|
+
*/
|
|
3
9
|
export function useInteractionState() {
|
|
4
10
|
const [state, setState] = useState(false);
|
|
5
11
|
|
|
@@ -10,7 +16,7 @@ export function useInteractionState() {
|
|
|
10
16
|
setState(false);
|
|
11
17
|
}, []);
|
|
12
18
|
|
|
13
|
-
return { state, onIn, onOut };
|
|
19
|
+
return useMemo(() => ({ state, onIn, onOut }), [state, onIn, onOut]);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,10 @@ export type {
|
|
|
40
40
|
export { useInteractionState } from './hooks/useInteractionState';
|
|
41
41
|
export { useDelayedLoading } from './hooks/useDelayedLoading';
|
|
42
42
|
export { useThrottledValue } from './hooks/useThrottledValue';
|
|
43
|
+
export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
44
|
+
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
45
|
+
export { useGutters } from './hooks/useGutters';
|
|
46
|
+
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
43
47
|
|
|
44
48
|
// Icons
|
|
45
49
|
export * as Icons from './icons';
|
|
@@ -97,12 +101,28 @@ export * as Typography from './typography';
|
|
|
97
101
|
export * as Skeleton from './skeleton';
|
|
98
102
|
export * as Grid from './grid';
|
|
99
103
|
export { Fill } from './fill';
|
|
104
|
+
export { MediaInsetBorder } from './media-inset-border';
|
|
105
|
+
export type { MediaInsetBorderProps } from './media-inset-border';
|
|
100
106
|
export { IconCircle } from './icon-circle';
|
|
101
107
|
export { ConnectionDots } from './connection-dots';
|
|
102
108
|
export type { ConnectionDotsProps } from './connection-dots';
|
|
103
109
|
export { BenefitRow, BenefitList } from './benefit-list';
|
|
104
110
|
export type { BenefitRowProps, BenefitListProps } from './benefit-list';
|
|
105
111
|
|
|
112
|
+
// Interaction primitives
|
|
113
|
+
export { PressableScale } from './pressable-scale';
|
|
114
|
+
export type { PressableScaleProps } from './pressable-scale';
|
|
115
|
+
export { PressableWithHover } from './pressable-with-hover';
|
|
116
|
+
export type { PressableWithHoverProps } from './pressable-with-hover';
|
|
117
|
+
export { SubtleHover } from './subtle-hover';
|
|
118
|
+
export type { SubtleHoverProps } from './subtle-hover';
|
|
119
|
+
|
|
120
|
+
// Motion presets (Reanimated enter/exit + directional screen transition)
|
|
121
|
+
export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion';
|
|
122
|
+
export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion';
|
|
123
|
+
export { AnimatedCheck } from './animated-check';
|
|
124
|
+
export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
125
|
+
|
|
106
126
|
// Form components
|
|
107
127
|
export * from './text-field';
|
|
108
128
|
export * from './segmented-control';
|
package/src/index.web.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type {
|
|
|
45
45
|
export { useInteractionState } from './hooks/useInteractionState';
|
|
46
46
|
export { useDelayedLoading } from './hooks/useDelayedLoading';
|
|
47
47
|
export { useThrottledValue } from './hooks/useThrottledValue';
|
|
48
|
+
export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
49
|
+
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
50
|
+
export { useGutters } from './hooks/useGutters';
|
|
51
|
+
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
48
52
|
|
|
49
53
|
// Icons
|
|
50
54
|
export * as Icons from './icons';
|
|
@@ -102,12 +106,28 @@ export * as Typography from './typography';
|
|
|
102
106
|
export * as Skeleton from './skeleton';
|
|
103
107
|
export * as Grid from './grid';
|
|
104
108
|
export { Fill } from './fill';
|
|
109
|
+
export { MediaInsetBorder } from './media-inset-border';
|
|
110
|
+
export type { MediaInsetBorderProps } from './media-inset-border';
|
|
105
111
|
export { IconCircle } from './icon-circle';
|
|
106
112
|
export { ConnectionDots } from './connection-dots/index.web';
|
|
107
113
|
export type { ConnectionDotsProps } from './connection-dots/index.web';
|
|
108
114
|
export { BenefitRow, BenefitList } from './benefit-list';
|
|
109
115
|
export type { BenefitRowProps, BenefitListProps } from './benefit-list';
|
|
110
116
|
|
|
117
|
+
// Interaction primitives
|
|
118
|
+
export { PressableScale } from './pressable-scale';
|
|
119
|
+
export type { PressableScaleProps } from './pressable-scale';
|
|
120
|
+
export { PressableWithHover } from './pressable-with-hover';
|
|
121
|
+
export type { PressableWithHoverProps } from './pressable-with-hover';
|
|
122
|
+
export { SubtleHover } from './subtle-hover';
|
|
123
|
+
export type { SubtleHoverProps } from './subtle-hover';
|
|
124
|
+
|
|
125
|
+
// Motion presets (Reanimated enter/exit + directional screen transition)
|
|
126
|
+
export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop, ScreenTransition } from './motion';
|
|
127
|
+
export type { ScreenTransitionProps, ScreenTransitionDirection } from './motion';
|
|
128
|
+
export { AnimatedCheck } from './animated-check';
|
|
129
|
+
export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
130
|
+
|
|
111
131
|
// Form components
|
|
112
132
|
export * from './text-field';
|
|
113
133
|
export * from './segmented-control';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Image, View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { MediaInsetBorder } from './index';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof MediaInsetBorder> = {
|
|
8
|
+
title: 'Components/MediaInsetBorder',
|
|
9
|
+
component: MediaInsetBorder,
|
|
10
|
+
argTypes: {
|
|
11
|
+
opaque: { control: 'boolean' },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof MediaInsetBorder>;
|
|
18
|
+
|
|
19
|
+
const SAMPLE_URI =
|
|
20
|
+
'https://images.unsplash.com/photo-1526779259212-939e64788e3c?w=400&q=80';
|
|
21
|
+
|
|
22
|
+
export const OverImage: Story = {
|
|
23
|
+
render: (args) => (
|
|
24
|
+
<View style={{ width: 240, height: 160, borderRadius: 8, overflow: 'hidden' }}>
|
|
25
|
+
<Image source={{ uri: SAMPLE_URI }} style={{ width: '100%', height: '100%' }} />
|
|
26
|
+
<MediaInsetBorder {...args} />
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const OverFlatSurface: Story = {
|
|
32
|
+
render: (args) => (
|
|
33
|
+
<View
|
|
34
|
+
style={{
|
|
35
|
+
width: 240,
|
|
36
|
+
height: 160,
|
|
37
|
+
borderRadius: 8,
|
|
38
|
+
backgroundColor: '#e5e7eb',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<MediaInsetBorder {...args} />
|
|
42
|
+
</View>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Opaque: Story = {
|
|
47
|
+
args: { opaque: true },
|
|
48
|
+
render: (args) => (
|
|
49
|
+
<View
|
|
50
|
+
style={{
|
|
51
|
+
width: 240,
|
|
52
|
+
height: 160,
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
backgroundColor: '#e5e7eb',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<MediaInsetBorder {...args} />
|
|
58
|
+
</View>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PixelRatio,
|
|
4
|
+
Platform,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
import { Fill } from '../fill';
|
|
11
|
+
import { borderRadius } from '../styles/tokens';
|
|
12
|
+
import { useTheme } from '../theme';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A 1px (hairline) inset border reads too heavy on high-DPI web displays, so we
|
|
16
|
+
* drop to 0.5px there. On native the platform hairline is already correct, and
|
|
17
|
+
* `PixelRatio` is only consulted on web.
|
|
18
|
+
*/
|
|
19
|
+
const HAIRLINE_WIDTH =
|
|
20
|
+
Platform.OS === 'web' && PixelRatio.get() > 1 ? 0.5 : StyleSheet.hairlineWidth;
|
|
21
|
+
|
|
22
|
+
export interface MediaInsetBorderProps {
|
|
23
|
+
/**
|
|
24
|
+
* Render a solid, full-contrast border instead of the default softened one.
|
|
25
|
+
* Use where the inset border must line up with adjacent opaque borders, such
|
|
26
|
+
* as external link-preview cards.
|
|
27
|
+
*/
|
|
28
|
+
opaque?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extra style merged last. Override `borderRadius` here to match the corner
|
|
31
|
+
* radius of the surrounding media box.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
children?: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A theme-aware hairline border painted just inside a media box (image, avatar,
|
|
39
|
+
* embed) to separate it from the surrounding background. Absolutely positioned
|
|
40
|
+
* to fill its parent (built on {@link Fill}) and non-interactive.
|
|
41
|
+
*/
|
|
42
|
+
export function MediaInsetBorder({ opaque, style, children }: MediaInsetBorderProps) {
|
|
43
|
+
const theme = useTheme();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Fill
|
|
47
|
+
style={[
|
|
48
|
+
styles.base,
|
|
49
|
+
{ borderColor: theme.colors.border },
|
|
50
|
+
!opaque && styles.soft,
|
|
51
|
+
style,
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</Fill>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
MediaInsetBorder.displayName = 'MediaInsetBorder';
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
base: {
|
|
63
|
+
borderRadius: borderRadius.sm,
|
|
64
|
+
borderWidth: HAIRLINE_WIDTH,
|
|
65
|
+
pointerEvents: 'none',
|
|
66
|
+
},
|
|
67
|
+
// Non-opaque: same border color at reduced opacity so it reads as a subtle,
|
|
68
|
+
// lower-contrast separator rather than a hard edge.
|
|
69
|
+
soft: {
|
|
70
|
+
opacity: 0.6,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, Text, View } from 'react-native';
|
|
3
|
+
import Animated from 'react-native-reanimated';
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
5
|
+
|
|
6
|
+
import { ScaleAndFadeIn, ScaleAndFadeOut, ScreenTransition, ShrinkAndPop } from './index';
|
|
7
|
+
|
|
8
|
+
const meta: Meta = {
|
|
9
|
+
title: 'Motion/Presets',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj;
|
|
15
|
+
|
|
16
|
+
function Toggle({ children }: { children: React.ReactNode }) {
|
|
17
|
+
const [shown, setShown] = useState(true);
|
|
18
|
+
return (
|
|
19
|
+
<View style={{ gap: 16, alignItems: 'flex-start' }}>
|
|
20
|
+
<Pressable
|
|
21
|
+
accessibilityLabel="Toggle"
|
|
22
|
+
onPress={() => setShown((v) => !v)}
|
|
23
|
+
style={{
|
|
24
|
+
paddingVertical: 8,
|
|
25
|
+
paddingHorizontal: 16,
|
|
26
|
+
borderRadius: 8,
|
|
27
|
+
backgroundColor: '#111827',
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>{shown ? 'Hide' : 'Show'}</Text>
|
|
31
|
+
</Pressable>
|
|
32
|
+
<View style={{ height: 96, justifyContent: 'center' }}>{shown ? children : null}</View>
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const card = {
|
|
38
|
+
width: 160,
|
|
39
|
+
height: 72,
|
|
40
|
+
borderRadius: 16,
|
|
41
|
+
backgroundColor: '#6366f1',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export const ScaleAndFade: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<Toggle>
|
|
49
|
+
<Animated.View entering={ScaleAndFadeIn} exiting={ScaleAndFadeOut} style={card}>
|
|
50
|
+
<Text style={{ color: 'white', fontWeight: '700' }}>Scale + Fade</Text>
|
|
51
|
+
</Animated.View>
|
|
52
|
+
</Toggle>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Pop: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Toggle>
|
|
59
|
+
<Animated.View entering={ScaleAndFadeIn} exiting={ShrinkAndPop} style={card}>
|
|
60
|
+
<Text style={{ color: 'white', fontWeight: '700' }}>Shrink + Pop</Text>
|
|
61
|
+
</Animated.View>
|
|
62
|
+
</Toggle>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const Transition: Story = {
|
|
67
|
+
render: () => {
|
|
68
|
+
function Demo() {
|
|
69
|
+
const [forward, setForward] = useState(true);
|
|
70
|
+
return (
|
|
71
|
+
<View style={{ gap: 16, alignItems: 'flex-start' }}>
|
|
72
|
+
<Pressable
|
|
73
|
+
accessibilityLabel="Swap direction"
|
|
74
|
+
onPress={() => setForward((v) => !v)}
|
|
75
|
+
style={{
|
|
76
|
+
paddingVertical: 8,
|
|
77
|
+
paddingHorizontal: 16,
|
|
78
|
+
borderRadius: 8,
|
|
79
|
+
backgroundColor: '#111827',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>
|
|
83
|
+
Direction: {forward ? 'forward' : 'backward'}
|
|
84
|
+
</Text>
|
|
85
|
+
</Pressable>
|
|
86
|
+
<ScreenTransition
|
|
87
|
+
key={forward ? 'forward' : 'backward'}
|
|
88
|
+
direction={forward ? 'forward' : 'backward'}
|
|
89
|
+
enabledWeb
|
|
90
|
+
style={card}
|
|
91
|
+
>
|
|
92
|
+
<Text style={{ color: 'white', fontWeight: '700' }}>Screen</Text>
|
|
93
|
+
</ScreenTransition>
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return <Demo />;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Platform, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
FadeIn,
|
|
6
|
+
FadeOut,
|
|
7
|
+
SlideInLeft,
|
|
8
|
+
SlideInRight,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
/** Direction the incoming screen travels. */
|
|
12
|
+
export type ScreenTransitionDirection = 'forward' | 'backward';
|
|
13
|
+
|
|
14
|
+
export interface ScreenTransitionProps {
|
|
15
|
+
/**
|
|
16
|
+
* `'forward'` slides the new screen in from the right, `'backward'` from the
|
|
17
|
+
* left — matching a push / pop navigation gesture.
|
|
18
|
+
*/
|
|
19
|
+
direction: ScreenTransitionDirection;
|
|
20
|
+
/**
|
|
21
|
+
* Enable the transition on web. Off by default: Reanimated layout animations
|
|
22
|
+
* are fragile across web bundlers, so web opts in explicitly and gets a
|
|
23
|
+
* lightweight cross-fade rather than a slide.
|
|
24
|
+
*/
|
|
25
|
+
enabledWeb?: boolean;
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** CSS-smooth deceleration for the native slide. */
|
|
31
|
+
const SLIDE_EASING = Easing.out(Easing.exp);
|
|
32
|
+
/** Web cross-fade duration (ms) — "totally vibes based", per the Bluesky original. */
|
|
33
|
+
const WEB_FADE_DURATION = 90;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wraps `children` in a Reanimated layout-animated view that slides in
|
|
37
|
+
* directionally on native and (optionally) cross-fades on web. Ported from
|
|
38
|
+
* Bluesky's `ScreenTransition`.
|
|
39
|
+
*
|
|
40
|
+
* On web the slide is intentionally dropped: layout animations there are
|
|
41
|
+
* unreliable, so `enabledWeb` gates a simple fade and the default is no
|
|
42
|
+
* transition at all.
|
|
43
|
+
*/
|
|
44
|
+
export function ScreenTransition({
|
|
45
|
+
direction,
|
|
46
|
+
enabledWeb,
|
|
47
|
+
style,
|
|
48
|
+
children,
|
|
49
|
+
}: ScreenTransitionProps) {
|
|
50
|
+
const isWeb = Platform.OS === 'web';
|
|
51
|
+
|
|
52
|
+
const entering = isWeb
|
|
53
|
+
? enabledWeb
|
|
54
|
+
? FadeIn.duration(WEB_FADE_DURATION)
|
|
55
|
+
: undefined
|
|
56
|
+
: (direction === 'forward' ? SlideInRight : SlideInLeft).easing(SLIDE_EASING);
|
|
57
|
+
|
|
58
|
+
const exiting = isWeb
|
|
59
|
+
? enabledWeb
|
|
60
|
+
? FadeOut.duration(WEB_FADE_DURATION)
|
|
61
|
+
: undefined
|
|
62
|
+
: FadeOut.duration(WEB_FADE_DURATION);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Animated.View entering={entering} exiting={exiting} style={style}>
|
|
66
|
+
{children}
|
|
67
|
+
</Animated.View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ScreenTransition.displayName = 'ScreenTransition';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withDelay,
|
|
3
|
+
withSequence,
|
|
4
|
+
withTiming,
|
|
5
|
+
type LayoutAnimation,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reanimated custom layout-animation presets. Each is a worklet builder
|
|
10
|
+
* compatible with an `Animated.View`'s `entering` / `exiting` prop:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Animated.View entering={ScaleAndFadeIn} exiting={ScaleAndFadeOut} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Ported from Bluesky's custom-animation set. The `'worklet'` directive is
|
|
17
|
+
* preserved through Bloom's build; the consuming app's Metro + reanimated Babel
|
|
18
|
+
* plugin transforms it so the animation runs on the UI thread (same path the
|
|
19
|
+
* existing `bottom-sheet` gesture worklets rely on).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Entering: fade in while growing from 70% to full size. */
|
|
23
|
+
export const ScaleAndFadeIn: () => LayoutAnimation = () => {
|
|
24
|
+
'worklet';
|
|
25
|
+
return {
|
|
26
|
+
animations: {
|
|
27
|
+
opacity: withTiming(1),
|
|
28
|
+
transform: [{ scale: withTiming(1) }],
|
|
29
|
+
},
|
|
30
|
+
initialValues: {
|
|
31
|
+
opacity: 0,
|
|
32
|
+
transform: [{ scale: 0.7 }],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Exiting counterpart to {@link ScaleAndFadeIn}: fade out while shrinking to 70%. */
|
|
38
|
+
export const ScaleAndFadeOut: () => LayoutAnimation = () => {
|
|
39
|
+
'worklet';
|
|
40
|
+
return {
|
|
41
|
+
animations: {
|
|
42
|
+
opacity: withTiming(0),
|
|
43
|
+
transform: [{ scale: withTiming(0.7) }],
|
|
44
|
+
},
|
|
45
|
+
initialValues: {
|
|
46
|
+
opacity: 1,
|
|
47
|
+
transform: [{ scale: 1 }],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Exiting with a brief "pop": the scale dips to 70%, overshoots to 110%, and the
|
|
54
|
+
* element fades out. Good for confirming a tapped or removed item.
|
|
55
|
+
*/
|
|
56
|
+
export const ShrinkAndPop: () => LayoutAnimation = () => {
|
|
57
|
+
'worklet';
|
|
58
|
+
return {
|
|
59
|
+
animations: {
|
|
60
|
+
opacity: withDelay(125, withTiming(0, { duration: 125 })),
|
|
61
|
+
transform: [
|
|
62
|
+
{
|
|
63
|
+
scale: withSequence(
|
|
64
|
+
withTiming(0.7, { duration: 75 }),
|
|
65
|
+
withTiming(1.1, { duration: 150 }),
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
initialValues: {
|
|
71
|
+
opacity: 1,
|
|
72
|
+
transform: [{ scale: 1 }],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|