@oxyhq/bloom 0.27.0 → 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.
Files changed (253) hide show
  1. package/lib/commonjs/animated-check/AnimatedCheck.js +162 -0
  2. package/lib/commonjs/animated-check/AnimatedCheck.js.map +1 -0
  3. package/lib/commonjs/animated-check/index.js +13 -0
  4. package/lib/commonjs/animated-check/index.js.map +1 -0
  5. package/lib/commonjs/dialog/placement.js +9 -7
  6. package/lib/commonjs/dialog/placement.js.map +1 -1
  7. package/lib/commonjs/fill/index.js.map +1 -1
  8. package/lib/commonjs/hooks/index.js +20 -0
  9. package/lib/commonjs/hooks/index.js.map +1 -1
  10. package/lib/commonjs/hooks/useGutters.js +106 -0
  11. package/lib/commonjs/hooks/useGutters.js.map +1 -0
  12. package/lib/commonjs/hooks/useHaptics.js +69 -0
  13. package/lib/commonjs/hooks/useHaptics.js.map +1 -0
  14. package/lib/commonjs/hooks/useInteractionState.js +8 -2
  15. package/lib/commonjs/hooks/useInteractionState.js.map +1 -1
  16. package/lib/commonjs/index.js +155 -63
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/index.web.js +155 -63
  19. package/lib/commonjs/index.web.js.map +1 -1
  20. package/lib/commonjs/media-inset-border/MediaInsetBorder.js +50 -0
  21. package/lib/commonjs/media-inset-border/MediaInsetBorder.js.map +1 -0
  22. package/lib/commonjs/media-inset-border/index.js +13 -0
  23. package/lib/commonjs/media-inset-border/index.js.map +1 -0
  24. package/lib/commonjs/motion/ScreenTransition.js +46 -0
  25. package/lib/commonjs/motion/ScreenTransition.js.map +1 -0
  26. package/lib/commonjs/motion/index.js +32 -0
  27. package/lib/commonjs/motion/index.js.map +1 -0
  28. package/lib/commonjs/motion/motion.js +93 -0
  29. package/lib/commonjs/motion/motion.js.map +1 -0
  30. package/lib/commonjs/pressable-scale/PressableScale.js +75 -0
  31. package/lib/commonjs/pressable-scale/PressableScale.js.map +1 -0
  32. package/lib/commonjs/pressable-scale/index.js +13 -0
  33. package/lib/commonjs/pressable-scale/index.js.map +1 -0
  34. package/lib/commonjs/pressable-with-hover/PressableWithHover.js +36 -0
  35. package/lib/commonjs/pressable-with-hover/PressableWithHover.js.map +1 -0
  36. package/lib/commonjs/pressable-with-hover/index.js +13 -0
  37. package/lib/commonjs/pressable-with-hover/index.js.map +1 -0
  38. package/lib/commonjs/styles/breakpoints.js +24 -0
  39. package/lib/commonjs/styles/breakpoints.js.map +1 -0
  40. package/lib/commonjs/styles/index.js +7 -0
  41. package/lib/commonjs/styles/index.js.map +1 -1
  42. package/lib/commonjs/subtle-hover/SubtleHover.js +69 -0
  43. package/lib/commonjs/subtle-hover/SubtleHover.js.map +1 -0
  44. package/lib/commonjs/subtle-hover/index.js +13 -0
  45. package/lib/commonjs/subtle-hover/index.js.map +1 -0
  46. package/lib/commonjs/theme/build-theme.js +2 -0
  47. package/lib/commonjs/theme/build-theme.js.map +1 -1
  48. package/lib/commonjs/theme/gradients.js +39 -0
  49. package/lib/commonjs/theme/gradients.js.map +1 -0
  50. package/lib/commonjs/theme/index.js +7 -0
  51. package/lib/commonjs/theme/index.js.map +1 -1
  52. package/lib/module/animated-check/AnimatedCheck.js +157 -0
  53. package/lib/module/animated-check/AnimatedCheck.js.map +1 -0
  54. package/lib/module/animated-check/index.js +4 -0
  55. package/lib/module/animated-check/index.js.map +1 -0
  56. package/lib/module/dialog/placement.js +9 -7
  57. package/lib/module/dialog/placement.js.map +1 -1
  58. package/lib/module/fill/index.js.map +1 -1
  59. package/lib/module/hooks/index.js +2 -0
  60. package/lib/module/hooks/index.js.map +1 -1
  61. package/lib/module/hooks/useGutters.js +103 -0
  62. package/lib/module/hooks/useGutters.js.map +1 -0
  63. package/lib/module/hooks/useHaptics.js +65 -0
  64. package/lib/module/hooks/useHaptics.js.map +1 -0
  65. package/lib/module/hooks/useInteractionState.js +10 -3
  66. package/lib/module/hooks/useInteractionState.js.map +1 -1
  67. package/lib/module/index.js +10 -1
  68. package/lib/module/index.js.map +1 -1
  69. package/lib/module/index.web.js +10 -1
  70. package/lib/module/index.web.js.map +1 -1
  71. package/lib/module/media-inset-border/MediaInsetBorder.js +47 -0
  72. package/lib/module/media-inset-border/MediaInsetBorder.js.map +1 -0
  73. package/lib/module/media-inset-border/index.js +4 -0
  74. package/lib/module/media-inset-border/index.js.map +1 -0
  75. package/lib/module/motion/ScreenTransition.js +40 -0
  76. package/lib/module/motion/ScreenTransition.js.map +1 -0
  77. package/lib/module/motion/index.js +5 -0
  78. package/lib/module/motion/index.js.map +1 -0
  79. package/lib/module/motion/motion.js +87 -0
  80. package/lib/module/motion/motion.js.map +1 -0
  81. package/lib/module/pressable-scale/PressableScale.js +72 -0
  82. package/lib/module/pressable-scale/PressableScale.js.map +1 -0
  83. package/lib/module/pressable-scale/index.js +4 -0
  84. package/lib/module/pressable-scale/index.js.map +1 -0
  85. package/lib/module/pressable-with-hover/PressableWithHover.js +31 -0
  86. package/lib/module/pressable-with-hover/PressableWithHover.js.map +1 -0
  87. package/lib/module/pressable-with-hover/index.js +4 -0
  88. package/lib/module/pressable-with-hover/index.js.map +1 -0
  89. package/lib/module/styles/breakpoints.js +20 -0
  90. package/lib/module/styles/breakpoints.js.map +1 -0
  91. package/lib/module/styles/index.js +1 -0
  92. package/lib/module/styles/index.js.map +1 -1
  93. package/lib/module/subtle-hover/SubtleHover.js +66 -0
  94. package/lib/module/subtle-hover/SubtleHover.js.map +1 -0
  95. package/lib/module/subtle-hover/index.js +4 -0
  96. package/lib/module/subtle-hover/index.js.map +1 -0
  97. package/lib/module/theme/build-theme.js +2 -0
  98. package/lib/module/theme/build-theme.js.map +1 -1
  99. package/lib/module/theme/gradients.js +35 -0
  100. package/lib/module/theme/gradients.js.map +1 -0
  101. package/lib/module/theme/index.js +1 -0
  102. package/lib/module/theme/index.js.map +1 -1
  103. package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts +18 -0
  104. package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/animated-check/index.d.ts +3 -0
  106. package/lib/typescript/commonjs/animated-check/index.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/dialog/placement.d.ts +8 -7
  108. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/fill/index.d.ts +2 -2
  110. package/lib/typescript/commonjs/fill/index.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
  112. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/hooks/useGutters.d.ts +32 -0
  114. package/lib/typescript/commonjs/hooks/useGutters.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/hooks/useHaptics.d.ts +28 -0
  116. package/lib/typescript/commonjs/hooks/useHaptics.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/hooks/useInteractionState.d.ts +6 -0
  118. package/lib/typescript/commonjs/hooks/useInteractionState.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/index.d.ts +16 -0
  120. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/index.web.d.ts +16 -0
  122. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts +26 -0
  124. package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/media-inset-border/index.d.ts +3 -0
  126. package/lib/typescript/commonjs/media-inset-border/index.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/motion/ScreenTransition.d.ts +33 -0
  128. package/lib/typescript/commonjs/motion/ScreenTransition.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/motion/index.d.ts +4 -0
  130. package/lib/typescript/commonjs/motion/index.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/motion/motion.d.ts +24 -0
  132. package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts +21 -0
  134. package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/pressable-scale/index.d.ts +3 -0
  136. package/lib/typescript/commonjs/pressable-scale/index.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts +18 -0
  138. package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/pressable-with-hover/index.d.ts +3 -0
  140. package/lib/typescript/commonjs/pressable-with-hover/index.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/styles/breakpoints.d.ts +18 -0
  142. package/lib/typescript/commonjs/styles/breakpoints.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/styles/index.d.ts +2 -0
  144. package/lib/typescript/commonjs/styles/index.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts +32 -0
  146. package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts.map +1 -0
  147. package/lib/typescript/commonjs/subtle-hover/index.d.ts +3 -0
  148. package/lib/typescript/commonjs/subtle-hover/index.d.ts.map +1 -0
  149. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/theme/gradients.d.ts +12 -0
  151. package/lib/typescript/commonjs/theme/gradients.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/theme/index.d.ts +2 -1
  153. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/theme/types.d.ts +11 -0
  155. package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
  156. package/lib/typescript/module/animated-check/AnimatedCheck.d.ts +18 -0
  157. package/lib/typescript/module/animated-check/AnimatedCheck.d.ts.map +1 -0
  158. package/lib/typescript/module/animated-check/index.d.ts +3 -0
  159. package/lib/typescript/module/animated-check/index.d.ts.map +1 -0
  160. package/lib/typescript/module/dialog/placement.d.ts +8 -7
  161. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  162. package/lib/typescript/module/fill/index.d.ts +2 -2
  163. package/lib/typescript/module/fill/index.d.ts.map +1 -1
  164. package/lib/typescript/module/hooks/index.d.ts +4 -0
  165. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  166. package/lib/typescript/module/hooks/useGutters.d.ts +32 -0
  167. package/lib/typescript/module/hooks/useGutters.d.ts.map +1 -0
  168. package/lib/typescript/module/hooks/useHaptics.d.ts +28 -0
  169. package/lib/typescript/module/hooks/useHaptics.d.ts.map +1 -0
  170. package/lib/typescript/module/hooks/useInteractionState.d.ts +6 -0
  171. package/lib/typescript/module/hooks/useInteractionState.d.ts.map +1 -1
  172. package/lib/typescript/module/index.d.ts +16 -0
  173. package/lib/typescript/module/index.d.ts.map +1 -1
  174. package/lib/typescript/module/index.web.d.ts +16 -0
  175. package/lib/typescript/module/index.web.d.ts.map +1 -1
  176. package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts +26 -0
  177. package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
  178. package/lib/typescript/module/media-inset-border/index.d.ts +3 -0
  179. package/lib/typescript/module/media-inset-border/index.d.ts.map +1 -0
  180. package/lib/typescript/module/motion/ScreenTransition.d.ts +33 -0
  181. package/lib/typescript/module/motion/ScreenTransition.d.ts.map +1 -0
  182. package/lib/typescript/module/motion/index.d.ts +4 -0
  183. package/lib/typescript/module/motion/index.d.ts.map +1 -0
  184. package/lib/typescript/module/motion/motion.d.ts +24 -0
  185. package/lib/typescript/module/motion/motion.d.ts.map +1 -0
  186. package/lib/typescript/module/pressable-scale/PressableScale.d.ts +21 -0
  187. package/lib/typescript/module/pressable-scale/PressableScale.d.ts.map +1 -0
  188. package/lib/typescript/module/pressable-scale/index.d.ts +3 -0
  189. package/lib/typescript/module/pressable-scale/index.d.ts.map +1 -0
  190. package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts +18 -0
  191. package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
  192. package/lib/typescript/module/pressable-with-hover/index.d.ts +3 -0
  193. package/lib/typescript/module/pressable-with-hover/index.d.ts.map +1 -0
  194. package/lib/typescript/module/styles/breakpoints.d.ts +18 -0
  195. package/lib/typescript/module/styles/breakpoints.d.ts.map +1 -0
  196. package/lib/typescript/module/styles/index.d.ts +2 -0
  197. package/lib/typescript/module/styles/index.d.ts.map +1 -1
  198. package/lib/typescript/module/subtle-hover/SubtleHover.d.ts +32 -0
  199. package/lib/typescript/module/subtle-hover/SubtleHover.d.ts.map +1 -0
  200. package/lib/typescript/module/subtle-hover/index.d.ts +3 -0
  201. package/lib/typescript/module/subtle-hover/index.d.ts.map +1 -0
  202. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  203. package/lib/typescript/module/theme/gradients.d.ts +12 -0
  204. package/lib/typescript/module/theme/gradients.d.ts.map +1 -0
  205. package/lib/typescript/module/theme/index.d.ts +2 -1
  206. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  207. package/lib/typescript/module/theme/types.d.ts +11 -0
  208. package/lib/typescript/module/theme/types.d.ts.map +1 -1
  209. package/package.json +72 -1
  210. package/src/__tests__/AnimatedCheck.test.tsx +31 -0
  211. package/src/__tests__/MediaInsetBorder.test.tsx +36 -0
  212. package/src/__tests__/Motion.test.tsx +58 -0
  213. package/src/__tests__/PressableScale.test.tsx +29 -0
  214. package/src/__tests__/PressableWithHover.test.tsx +48 -0
  215. package/src/__tests__/SubtleHover.test.tsx +42 -0
  216. package/src/__tests__/theme-gradients.test.ts +31 -0
  217. package/src/__tests__/theme.test.ts +3 -0
  218. package/src/__tests__/useGutters.test.ts +58 -0
  219. package/src/__tests__/useHaptics.test.tsx +58 -0
  220. package/src/__tests__/useInteractionState.test.ts +27 -0
  221. package/src/animated-check/AnimatedCheck.stories.tsx +60 -0
  222. package/src/animated-check/AnimatedCheck.tsx +173 -0
  223. package/src/animated-check/index.ts +2 -0
  224. package/src/dialog/placement.ts +10 -7
  225. package/src/fill/index.tsx +2 -2
  226. package/src/hooks/index.ts +4 -0
  227. package/src/hooks/useGutters.ts +99 -0
  228. package/src/hooks/useHaptics.tsx +69 -0
  229. package/src/hooks/useInteractionState.ts +8 -2
  230. package/src/index.ts +20 -0
  231. package/src/index.web.ts +20 -0
  232. package/src/media-inset-border/MediaInsetBorder.stories.tsx +60 -0
  233. package/src/media-inset-border/MediaInsetBorder.tsx +72 -0
  234. package/src/media-inset-border/index.ts +2 -0
  235. package/src/motion/Motion.stories.tsx +99 -0
  236. package/src/motion/ScreenTransition.tsx +71 -0
  237. package/src/motion/index.ts +3 -0
  238. package/src/motion/motion.ts +75 -0
  239. package/src/pressable-scale/PressableScale.stories.tsx +71 -0
  240. package/src/pressable-scale/PressableScale.tsx +103 -0
  241. package/src/pressable-scale/index.ts +2 -0
  242. package/src/pressable-with-hover/PressableWithHover.stories.tsx +43 -0
  243. package/src/pressable-with-hover/PressableWithHover.tsx +43 -0
  244. package/src/pressable-with-hover/index.ts +2 -0
  245. package/src/styles/breakpoints.ts +18 -0
  246. package/src/styles/index.ts +2 -0
  247. package/src/subtle-hover/SubtleHover.stories.tsx +72 -0
  248. package/src/subtle-hover/SubtleHover.tsx +90 -0
  249. package/src/subtle-hover/index.ts +2 -0
  250. package/src/theme/build-theme.ts +2 -0
  251. package/src/theme/gradients.ts +61 -0
  252. package/src/theme/index.ts +2 -1
  253. package/src/theme/types.ts +13 -0
@@ -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,3 @@
1
+ export { ScaleAndFadeIn, ScaleAndFadeOut, ShrinkAndPop } from './motion';
2
+ export { ScreenTransition } from './ScreenTransition';
3
+ export type { ScreenTransitionProps, ScreenTransitionDirection } from './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
+ };
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { PressableScale } from './index';
6
+
7
+ const meta: Meta<typeof PressableScale> = {
8
+ title: 'Components/PressableScale',
9
+ component: PressableScale,
10
+ argTypes: {
11
+ targetScale: { control: { type: 'number', min: 0.8, max: 1, step: 0.01 } },
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof PressableScale>;
18
+
19
+ export const Basic: Story = {
20
+ render: (args) => (
21
+ <PressableScale
22
+ {...args}
23
+ accessibilityLabel="Press me"
24
+ contentContainerStyle={{
25
+ paddingVertical: 12,
26
+ paddingHorizontal: 20,
27
+ borderRadius: 12,
28
+ backgroundColor: '#111827',
29
+ }}
30
+ >
31
+ <Text style={{ color: '#fff', fontWeight: '600' }}>Press me</Text>
32
+ </PressableScale>
33
+ ),
34
+ };
35
+
36
+ export const StrongScale: Story = {
37
+ args: { targetScale: 0.9 },
38
+ render: (args) => (
39
+ <PressableScale
40
+ {...args}
41
+ accessibilityLabel="Press me harder"
42
+ contentContainerStyle={{
43
+ paddingVertical: 12,
44
+ paddingHorizontal: 20,
45
+ borderRadius: 12,
46
+ backgroundColor: '#4f46e5',
47
+ }}
48
+ >
49
+ <Text style={{ color: '#fff', fontWeight: '600' }}>Strong scale (0.9)</Text>
50
+ </PressableScale>
51
+ ),
52
+ };
53
+
54
+ export const Card: Story = {
55
+ render: (args) => (
56
+ <PressableScale {...args} accessibilityLabel="Open card">
57
+ <View
58
+ style={{
59
+ width: 220,
60
+ padding: 16,
61
+ borderRadius: 16,
62
+ backgroundColor: '#f3f4f6',
63
+ gap: 6,
64
+ }}
65
+ >
66
+ <Text style={{ fontWeight: '700' }}>Tappable card</Text>
67
+ <Text style={{ color: '#6b7280' }}>The whole card springs on press.</Text>
68
+ </View>
69
+ </PressableScale>
70
+ ),
71
+ };
@@ -0,0 +1,103 @@
1
+ import React, { forwardRef } from 'react';
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ type PressableProps,
6
+ type StyleProp,
7
+ type View,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import Animated, {
11
+ cancelAnimation,
12
+ useAnimatedStyle,
13
+ useReducedMotion,
14
+ useSharedValue,
15
+ withTiming,
16
+ } from 'react-native-reanimated';
17
+
18
+ /** Press-in / press-out timing, in ms. */
19
+ const DURATION = 100;
20
+
21
+ /**
22
+ * True on touch-capable web browsers (coarse pointer). The press-scale
23
+ * affordance only makes sense where a finger obscures the element — with a
24
+ * mouse it reads as jitter — so it is disabled on non-touch web, matching the
25
+ * native / web-touch behaviour.
26
+ */
27
+ const IS_WEB_TOUCH_DEVICE =
28
+ Platform.OS === 'web' &&
29
+ typeof window !== 'undefined' &&
30
+ typeof window.matchMedia === 'function' &&
31
+ window.matchMedia('(pointer: coarse)').matches;
32
+
33
+ const SCALE_SUPPORTED = Platform.OS !== 'web' || IS_WEB_TOUCH_DEVICE;
34
+
35
+ export interface PressableScaleProps
36
+ extends Omit<PressableProps, 'children' | 'style'> {
37
+ /** Scale applied while pressed. Defaults to `0.98`. */
38
+ targetScale?: number;
39
+ /** Style for the outer, full-size touch target (stays put while pressing). */
40
+ style?: StyleProp<ViewStyle>;
41
+ /** Style for the inner content wrapper that actually scales. */
42
+ contentContainerStyle?: StyleProp<ViewStyle>;
43
+ children?: React.ReactNode;
44
+ }
45
+
46
+ /**
47
+ * A drop-in {@link Pressable} whose content springs to a smaller scale on
48
+ * press-in and back on release. The outer pressable stays full size (so the
49
+ * touch target never shrinks) while an inner wrapper scales.
50
+ *
51
+ * Honours the OS "reduce motion" setting and disables the effect on non-touch
52
+ * web pointers.
53
+ */
54
+ export const PressableScale = forwardRef<View, PressableScaleProps>(
55
+ function PressableScale(
56
+ {
57
+ targetScale = 0.98,
58
+ style,
59
+ contentContainerStyle,
60
+ children,
61
+ onPressIn,
62
+ onPressOut,
63
+ ...rest
64
+ },
65
+ ref,
66
+ ) {
67
+ const reducedMotion = useReducedMotion();
68
+ const animate = SCALE_SUPPORTED && !reducedMotion;
69
+
70
+ const scale = useSharedValue(1);
71
+
72
+ const animatedStyle = useAnimatedStyle(() => ({
73
+ transform: [{ scale: scale.value }],
74
+ }));
75
+
76
+ return (
77
+ <Pressable
78
+ ref={ref}
79
+ accessibilityRole="button"
80
+ onPressIn={(e) => {
81
+ onPressIn?.(e);
82
+ if (!animate) return;
83
+ cancelAnimation(scale);
84
+ scale.value = withTiming(targetScale, { duration: DURATION });
85
+ }}
86
+ onPressOut={(e) => {
87
+ onPressOut?.(e);
88
+ if (!animate) return;
89
+ cancelAnimation(scale);
90
+ scale.value = withTiming(1, { duration: DURATION });
91
+ }}
92
+ style={style}
93
+ {...rest}
94
+ >
95
+ <Animated.View style={[animate ? animatedStyle : null, contentContainerStyle]}>
96
+ {children}
97
+ </Animated.View>
98
+ </Pressable>
99
+ );
100
+ },
101
+ );
102
+
103
+ PressableScale.displayName = 'PressableScale';
@@ -0,0 +1,2 @@
1
+ export { PressableScale } from './PressableScale';
2
+ export type { PressableScaleProps } from './PressableScale';
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { PressableWithHover } from './index';
6
+
7
+ const meta: Meta<typeof PressableWithHover> = {
8
+ title: 'Components/PressableWithHover',
9
+ component: PressableWithHover,
10
+ };
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj<typeof PressableWithHover>;
15
+
16
+ export const Basic: Story = {
17
+ render: () => (
18
+ <PressableWithHover
19
+ accessibilityLabel="Hover me"
20
+ style={{
21
+ paddingVertical: 12,
22
+ paddingHorizontal: 20,
23
+ borderRadius: 12,
24
+ backgroundColor: '#f3f4f6',
25
+ }}
26
+ hoverStyle={{ backgroundColor: '#e5e7eb' }}
27
+ >
28
+ <Text style={{ fontWeight: '600' }}>Hover me (web)</Text>
29
+ </PressableWithHover>
30
+ ),
31
+ };
32
+
33
+ export const ListRow: Story = {
34
+ render: () => (
35
+ <PressableWithHover
36
+ accessibilityLabel="Settings row"
37
+ style={{ paddingVertical: 14, paddingHorizontal: 16, borderRadius: 8, width: 260 }}
38
+ hoverStyle={{ backgroundColor: '#f3f4f6' }}
39
+ >
40
+ <Text>Account settings</Text>
41
+ </PressableWithHover>
42
+ ),
43
+ };
@@ -0,0 +1,43 @@
1
+ import React, { forwardRef } from 'react';
2
+ import {
3
+ Pressable,
4
+ type PressableProps,
5
+ type StyleProp,
6
+ type View,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+
10
+ import { useInteractionState } from '../hooks/useInteractionState';
11
+
12
+ export interface PressableWithHoverProps extends Omit<PressableProps, 'style'> {
13
+ /** Base style; `hoverStyle` is merged on top while the pointer is over it. */
14
+ style?: StyleProp<ViewStyle>;
15
+ /**
16
+ * Style merged over `style` while hovered. Web-only: react-native-web fires
17
+ * `onHoverIn`/`onHoverOut`, so on touch platforms this is simply never applied.
18
+ */
19
+ hoverStyle: StyleProp<ViewStyle>;
20
+ }
21
+
22
+ /**
23
+ * A {@link Pressable} that merges `hoverStyle` on top of `style` while hovered.
24
+ * Hover is a web-only affordance (native platforms have no pointer hover), so
25
+ * this behaves like a plain `Pressable` on touch devices.
26
+ */
27
+ export const PressableWithHover = forwardRef<View, PressableWithHoverProps>(
28
+ function PressableWithHover({ style, hoverStyle, ...props }, ref) {
29
+ const { state: hovered, onIn: onHoverIn, onOut: onHoverOut } = useInteractionState();
30
+
31
+ return (
32
+ <Pressable
33
+ {...props}
34
+ ref={ref}
35
+ onHoverIn={onHoverIn}
36
+ onHoverOut={onHoverOut}
37
+ style={hovered ? [style, hoverStyle] : style}
38
+ />
39
+ );
40
+ },
41
+ );
42
+
43
+ PressableWithHover.displayName = 'PressableWithHover';
@@ -0,0 +1,2 @@
1
+ export { PressableWithHover } from './PressableWithHover';
2
+ export type { PressableWithHoverProps } from './PressableWithHover';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Bloom's responsive breakpoint min-widths (px), matching the Tailwind /
3
+ * NativeWind defaults used across the design system.
4
+ *
5
+ * This is the SINGLE SOURCE OF TRUTH for breakpoint values. Anything that
6
+ * branches on viewport width — the `<Dialog>` responsive `placement` resolver,
7
+ * the `useGutters` hook — reads from here rather than hardcoding its own
8
+ * numbers, so a change here propagates everywhere.
9
+ */
10
+ export const BREAKPOINTS = {
11
+ sm: 640,
12
+ md: 768,
13
+ lg: 1024,
14
+ xl: 1280,
15
+ } as const;
16
+
17
+ /** A named responsive breakpoint tier. */
18
+ export type Breakpoint = keyof typeof BREAKPOINTS;
@@ -1,6 +1,8 @@
1
1
  export { atoms, flatten } from './atoms';
2
2
  export type { ViewStyleProp, TextStyleProp } from './atoms';
3
3
  export * as tokens from './tokens';
4
+ export { BREAKPOINTS } from './breakpoints';
5
+ export type { Breakpoint } from './breakpoints';
4
6
  export { Z_INDEX, zIndex } from './z-index';
5
7
  export { web, native, ios, android, platform, select } from './platform';
6
8
  export { SUPPORTS_NATIVE_DRIVER } from './native-driver';
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { Pressable, Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { useInteractionState } from '../hooks/useInteractionState';
6
+ import { SubtleHover } from './index';
7
+
8
+ const meta: Meta<typeof SubtleHover> = {
9
+ title: 'Components/SubtleHover',
10
+ component: SubtleHover,
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof SubtleHover>;
16
+
17
+ /** A row that washes its background as the pointer enters (web). */
18
+ function HoverRow({ label, native }: { label: string; native?: boolean }) {
19
+ const { state: hovered, onIn, onOut } = useInteractionState();
20
+
21
+ return (
22
+ <Pressable
23
+ accessibilityLabel={label}
24
+ onHoverIn={onIn}
25
+ onHoverOut={onOut}
26
+ style={{
27
+ width: 280,
28
+ paddingVertical: 14,
29
+ paddingHorizontal: 16,
30
+ borderRadius: 12,
31
+ overflow: 'hidden',
32
+ }}
33
+ >
34
+ <SubtleHover hover={hovered} native={native} style={{ borderRadius: 12 }} />
35
+ <Text style={{ fontWeight: '600' }}>{label}</Text>
36
+ </Pressable>
37
+ );
38
+ }
39
+
40
+ export const ListRow: Story = {
41
+ render: () => <HoverRow label="Hover me (web)" />,
42
+ };
43
+
44
+ export const StrongerWash: Story = {
45
+ render: () => {
46
+ function Row() {
47
+ const { state: hovered, onIn, onOut } = useInteractionState();
48
+ return (
49
+ <Pressable
50
+ accessibilityLabel="Stronger wash"
51
+ onHoverIn={onIn}
52
+ onHoverOut={onOut}
53
+ style={{ width: 280, padding: 16, borderRadius: 12, overflow: 'hidden' }}
54
+ >
55
+ <SubtleHover hover={hovered} opacity={0.85} style={{ borderRadius: 12 }} />
56
+ <Text style={{ fontWeight: '600' }}>Higher opacity override</Text>
57
+ </Pressable>
58
+ );
59
+ }
60
+ return <Row />;
61
+ },
62
+ };
63
+
64
+ export const Stack: Story = {
65
+ render: () => (
66
+ <View style={{ gap: 4 }}>
67
+ <HoverRow label="Inbox" />
68
+ <HoverRow label="Sent" />
69
+ <HoverRow label="Drafts" />
70
+ </View>
71
+ ),
72
+ };