@oxyhq/bloom 0.1.12 → 0.1.14

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.
@@ -70,6 +70,66 @@ const SpinnerLoading: React.FC<SpinnerLoadingProps> = ({
70
70
  );
71
71
  };
72
72
 
73
+ /**
74
+ * Inner component for TopLoading that requires react-native-reanimated.
75
+ * Extracted to a dedicated component so all hooks are called unconditionally,
76
+ * satisfying the Rules of Hooks and React Compiler requirements.
77
+ */
78
+ type AnimatedTopLoadingProps = {
79
+ showLoading: boolean;
80
+ targetHeight: number;
81
+ effectiveIconSize: number;
82
+ spinnerColor: string;
83
+ spinnerIcon: React.ReactNode | undefined;
84
+ style: TopLoadingProps['style'];
85
+ testID: string | undefined;
86
+ reanimated: NonNullable<ReturnType<typeof getReanimated>>;
87
+ };
88
+
89
+ const AnimatedTopLoading: React.FC<AnimatedTopLoadingProps> = ({
90
+ showLoading,
91
+ targetHeight,
92
+ effectiveIconSize,
93
+ spinnerColor,
94
+ spinnerIcon,
95
+ style,
96
+ testID,
97
+ reanimated,
98
+ }) => {
99
+ const { default: Animated, useAnimatedStyle, useSharedValue, withTiming, Easing } = reanimated;
100
+
101
+ const height = useSharedValue(showLoading ? targetHeight : 0);
102
+ const opacity = useSharedValue(showLoading ? 1 : 0);
103
+ const translateY = useSharedValue(showLoading ? 0 : -targetHeight);
104
+
105
+ useEffect(() => {
106
+ const timingConfig = { duration: animation.duration.slow, easing: Easing.out(Easing.cubic) };
107
+ height.value = withTiming(showLoading ? targetHeight : 0, timingConfig);
108
+ opacity.value = withTiming(showLoading ? 1 : 0, timingConfig);
109
+ translateY.value = withTiming(showLoading ? 0 : -targetHeight, timingConfig);
110
+ // Easing, withTiming: module-level constants from lazily-resolved reanimated, stable.
111
+ // height/opacity/translateY: shared value objects, stable references.
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, [showLoading, targetHeight]);
114
+
115
+ const containerAnimated = useAnimatedStyle(() => ({
116
+ height: height.value,
117
+ }));
118
+
119
+ const innerAnimated = useAnimatedStyle(() => ({
120
+ opacity: opacity.value,
121
+ transform: [{ translateY: translateY.value }],
122
+ }));
123
+
124
+ return (
125
+ <Animated.View style={[styles.topContainer, containerAnimated]} testID={testID}>
126
+ <Animated.View style={[styles.topLoadingView, { height: targetHeight }, innerAnimated, style]}>
127
+ {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
128
+ </Animated.View>
129
+ </Animated.View>
130
+ );
131
+ };
132
+
73
133
  const TopLoading: React.FC<TopLoadingProps> = ({
74
134
  size = 'medium',
75
135
  color,
@@ -88,8 +148,8 @@ const TopLoading: React.FC<TopLoadingProps> = ({
88
148
 
89
149
  const reanimated = getReanimated();
90
150
 
91
- // Non-animated fallback when reanimated is not available
92
151
  if (!reanimated) {
152
+ // Non-animated fallback when reanimated is not available
93
153
  if (!showLoading) return null;
94
154
  return (
95
155
  <View style={[styles.topContainer, { height: targetHeight }, style]} testID={testID}>
@@ -100,41 +160,17 @@ const TopLoading: React.FC<TopLoadingProps> = ({
100
160
  );
101
161
  }
102
162
 
103
- const { default: Animated, useAnimatedStyle, useSharedValue, withTiming, Easing } = reanimated;
104
-
105
- // eslint-disable-next-line react-hooks/rules-of-hooks
106
- const height = useSharedValue(showLoading ? targetHeight : 0);
107
- // eslint-disable-next-line react-hooks/rules-of-hooks
108
- const opacity = useSharedValue(showLoading ? 1 : 0);
109
- // eslint-disable-next-line react-hooks/rules-of-hooks
110
- const translateY = useSharedValue(showLoading ? 0 : -targetHeight);
111
-
112
- const timingConfig = { duration: animation.duration.slow, easing: Easing.out(Easing.cubic) };
113
-
114
- // eslint-disable-next-line react-hooks/rules-of-hooks
115
- useEffect(() => {
116
- height.value = withTiming(showLoading ? targetHeight : 0, timingConfig);
117
- opacity.value = withTiming(showLoading ? 1 : 0, timingConfig);
118
- translateY.value = withTiming(showLoading ? 0 : -targetHeight, timingConfig);
119
- }, [showLoading, targetHeight, height, opacity, translateY]);
120
-
121
- // eslint-disable-next-line react-hooks/rules-of-hooks
122
- const containerAnimated = useAnimatedStyle(() => ({
123
- height: height.value,
124
- }));
125
-
126
- // eslint-disable-next-line react-hooks/rules-of-hooks
127
- const innerAnimated = useAnimatedStyle(() => ({
128
- opacity: opacity.value,
129
- transform: [{ translateY: translateY.value }],
130
- }));
131
-
132
163
  return (
133
- <Animated.View style={[styles.topContainer, containerAnimated]} testID={testID}>
134
- <Animated.View style={[styles.topLoadingView, { height: targetHeight }, innerAnimated, style]}>
135
- {spinnerIcon ?? <SpinnerIcon size={effectiveIconSize} color={spinnerColor} />}
136
- </Animated.View>
137
- </Animated.View>
164
+ <AnimatedTopLoading
165
+ showLoading={showLoading}
166
+ targetHeight={targetHeight}
167
+ effectiveIconSize={effectiveIconSize}
168
+ spinnerColor={spinnerColor}
169
+ spinnerIcon={spinnerIcon}
170
+ style={style}
171
+ testID={testID}
172
+ reanimated={reanimated}
173
+ />
138
174
  );
139
175
  };
140
176
 
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect } from 'react';
2
- import { ActivityIndicator, View, type ViewStyle } from 'react-native';
2
+ import { ActivityIndicator, type ViewStyle } from 'react-native';
3
3
 
4
4
  // Lazy-loaded dependencies for the SVG spinner.
5
5
  // Falls back to ActivityIndicator if react-native-svg or react-native-reanimated are not installed.
@@ -41,23 +41,22 @@ interface SpinnerIconProps {
41
41
  style?: ViewStyle;
42
42
  }
43
43
 
44
+ type AnimatedSpinnerProps = SpinnerIconProps & {
45
+ svg: NonNullable<SvgModuleType>;
46
+ reanimated: NonNullable<ReanimatedType>;
47
+ };
48
+
44
49
  /**
45
- * iOS-style SVG spinner with 8 rotating rectangles and an opacity gradient trail.
46
- * Requires react-native-svg and react-native-reanimated as peer dependencies.
47
- * Falls back to ActivityIndicator if either is missing.
50
+ * Inner component that unconditionally calls Reanimated hooks.
51
+ * Only rendered when both react-native-svg and react-native-reanimated are available.
48
52
  */
49
- export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
53
+ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
50
54
  color = '#005c67',
51
55
  size = 26,
52
56
  style,
57
+ svg,
58
+ reanimated,
53
59
  }) => {
54
- const svg = getSvgModule();
55
- const reanimated = getReanimated();
56
-
57
- if (!svg || !reanimated) {
58
- return <ActivityIndicator size={size > 30 ? 'large' : 'small'} color={color} />;
59
- }
60
-
61
60
  const { default: Svg, Rect } = svg;
62
61
  const {
63
62
  default: Animated,
@@ -68,19 +67,19 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
68
67
  Easing,
69
68
  } = reanimated;
70
69
 
71
- // eslint-disable-next-line react-hooks/rules-of-hooks
72
70
  const rotation = useSharedValue(0);
73
71
 
74
- // eslint-disable-next-line react-hooks/rules-of-hooks
75
72
  useEffect(() => {
76
73
  rotation.value = withRepeat(
77
74
  withTiming(360, { duration: 400, easing: Easing.linear }),
78
75
  -1,
79
- false
76
+ false,
80
77
  );
81
- }, [rotation, withRepeat, withTiming, Easing]);
78
+ // Reanimated shared values are stable references; withRepeat/withTiming/Easing
79
+ // are module-level functions from the lazily-loaded module and are stable too.
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, []);
82
82
 
83
- // eslint-disable-next-line react-hooks/rules-of-hooks
84
83
  const animatedStyle = useAnimatedStyle(() => ({
85
84
  transform: [{ rotate: `${rotation.value}deg` }],
86
85
  }));
@@ -112,4 +111,32 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
112
111
  );
113
112
  };
114
113
 
114
+ /**
115
+ * iOS-style SVG spinner with 8 rotating rectangles and an opacity gradient trail.
116
+ * Requires react-native-svg and react-native-reanimated as peer dependencies.
117
+ * Falls back to ActivityIndicator if either is missing.
118
+ */
119
+ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
120
+ color = '#005c67',
121
+ size = 26,
122
+ style,
123
+ }) => {
124
+ const svg = getSvgModule();
125
+ const reanimated = getReanimated();
126
+
127
+ if (!svg || !reanimated) {
128
+ return <ActivityIndicator size={size > 30 ? 'large' : 'small'} color={color} />;
129
+ }
130
+
131
+ return (
132
+ <AnimatedSpinner
133
+ color={color}
134
+ size={size}
135
+ style={style}
136
+ svg={svg}
137
+ reanimated={reanimated}
138
+ />
139
+ );
140
+ };
141
+
115
142
  SpinnerIcon.displayName = 'SpinnerIcon';