@oxyhq/bloom 0.27.0 → 0.29.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 +78 -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 +74 -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 +75 -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 +71 -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 +37 -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 +37 -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 +38 -0
  214. package/src/__tests__/PressableWithHover.test.tsx +48 -0
  215. package/src/__tests__/SubtleHover.test.tsx +50 -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 +92 -0
  240. package/src/pressable-scale/PressableScale.tsx +93 -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 +85 -0
  248. package/src/subtle-hover/SubtleHover.tsx +98 -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,92 @@
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
+ style={{
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
+ style={{
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
+ /**
55
+ * NativeWind layout classes apply to the SAME node that scales — the whole row
56
+ * (avatar + label) springs as one. In a NativeWind consumer you pass `className`
57
+ * directly; the spread-cast here just satisfies Bloom's own typecheck (which has
58
+ * no NativeWind ambient types).
59
+ */
60
+ export const InlineRow: Story = {
61
+ render: (args) => (
62
+ <PressableScale
63
+ {...args}
64
+ accessibilityLabel="Open profile"
65
+ {...({
66
+ className: 'flex-row items-center gap-3 px-4 py-3 rounded-xl bg-muted',
67
+ } as Record<string, string>)}
68
+ >
69
+ <View style={{ width: 32, height: 32, borderRadius: 16, backgroundColor: '#4f46e5' }} />
70
+ <Text style={{ fontWeight: '600' }}>Row that scales as one node</Text>
71
+ </PressableScale>
72
+ ),
73
+ };
74
+
75
+ export const Card: Story = {
76
+ render: (args) => (
77
+ <PressableScale {...args} accessibilityLabel="Open card">
78
+ <View
79
+ style={{
80
+ width: 220,
81
+ padding: 16,
82
+ borderRadius: 16,
83
+ backgroundColor: '#f3f4f6',
84
+ gap: 6,
85
+ }}
86
+ >
87
+ <Text style={{ fontWeight: '700' }}>Tappable card</Text>
88
+ <Text style={{ color: '#6b7280' }}>The whole card springs on press.</Text>
89
+ </View>
90
+ </PressableScale>
91
+ ),
92
+ };
@@ -0,0 +1,93 @@
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
+ /**
36
+ * A single animated `Pressable`, built once at module scope so the animated
37
+ * node identity is stable across renders.
38
+ */
39
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
40
+
41
+ export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
42
+ /** Scale applied while pressed. Defaults to `0.98`. */
43
+ targetScale?: number;
44
+ /** Style applied to the pressable itself — the node that scales. */
45
+ style?: StyleProp<ViewStyle>;
46
+ }
47
+
48
+ /**
49
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
50
+ * back on release. The scale is applied to the pressable node itself, so a
51
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
52
+ * the same node that animates.
53
+ *
54
+ * The touch target scales with the content — acceptable at the default ~0.98
55
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
56
+ * Honours the OS "reduce motion" setting and disables the effect on non-touch
57
+ * web pointers.
58
+ */
59
+ export const PressableScale = forwardRef<View, PressableScaleProps>(
60
+ function PressableScale({ targetScale = 0.98, style, onPressIn, onPressOut, ...rest }, ref) {
61
+ const reducedMotion = useReducedMotion();
62
+ const animate = SCALE_SUPPORTED && !reducedMotion;
63
+
64
+ const scale = useSharedValue(1);
65
+
66
+ const animatedStyle = useAnimatedStyle(() => ({
67
+ transform: [{ scale: scale.value }],
68
+ }));
69
+
70
+ return (
71
+ <AnimatedPressable
72
+ ref={ref}
73
+ accessibilityRole="button"
74
+ onPressIn={(e) => {
75
+ onPressIn?.(e);
76
+ if (!animate) return;
77
+ cancelAnimation(scale);
78
+ scale.value = withTiming(targetScale, { duration: DURATION });
79
+ }}
80
+ onPressOut={(e) => {
81
+ onPressOut?.(e);
82
+ if (!animate) return;
83
+ cancelAnimation(scale);
84
+ scale.value = withTiming(1, { duration: DURATION });
85
+ }}
86
+ style={[animate ? animatedStyle : null, style]}
87
+ {...rest}
88
+ />
89
+ );
90
+ },
91
+ );
92
+
93
+ 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,85 @@
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
+ /**
18
+ * Pure-CSS `group-hover`: the parent carries `className="group"` and the wash
19
+ * fades in on hover with ZERO React state (web only).
20
+ */
21
+ function HoverRow({ label }: { label: string }) {
22
+ return (
23
+ <Pressable
24
+ {...({ className: 'group' } as Record<string, string>)}
25
+ accessibilityLabel={label}
26
+ style={{
27
+ width: 280,
28
+ paddingVertical: 14,
29
+ paddingHorizontal: 16,
30
+ borderRadius: 12,
31
+ overflow: 'hidden',
32
+ }}
33
+ >
34
+ <SubtleHover style={{ borderRadius: 12 }} />
35
+ <Text style={{ fontWeight: '600' }}>{label}</Text>
36
+ </Pressable>
37
+ );
38
+ }
39
+
40
+ export const GroupHover: Story = {
41
+ render: () => <HoverRow label="Hover me (web, group-hover)" />,
42
+ };
43
+
44
+ export const Stack: Story = {
45
+ render: () => (
46
+ <View style={{ gap: 4 }}>
47
+ <HoverRow label="Inbox" />
48
+ <HoverRow label="Sent" />
49
+ <HoverRow label="Drafts" />
50
+ </View>
51
+ ),
52
+ };
53
+
54
+ /**
55
+ * JS `active` mode: hovering anywhere in the thread lights EVERY row together
56
+ * (a coordinated highlight a per-element `group-hover` can't express). `native`
57
+ * is opted in so the same wash shows on native surfaces too.
58
+ */
59
+ export const CoordinatedThread: Story = {
60
+ render: () => {
61
+ function Thread() {
62
+ const { state: hovered, onIn, onOut } = useInteractionState();
63
+ return (
64
+ <Pressable onHoverIn={onIn} onHoverOut={onOut} style={{ gap: 4 }}>
65
+ {['First reply', 'Second reply', 'Third reply'].map((label) => (
66
+ <View
67
+ key={label}
68
+ style={{
69
+ width: 280,
70
+ paddingVertical: 14,
71
+ paddingHorizontal: 16,
72
+ borderRadius: 12,
73
+ overflow: 'hidden',
74
+ }}
75
+ >
76
+ <SubtleHover active={hovered} native style={{ borderRadius: 12 }} />
77
+ <Text style={{ fontWeight: '600' }}>{label}</Text>
78
+ </View>
79
+ ))}
80
+ </Pressable>
81
+ );
82
+ }
83
+ return <Thread />;
84
+ },
85
+ };