@oxyhq/bloom 0.6.2 → 0.6.4

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.
@@ -1,15 +1,24 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { ActivityIndicator, type ViewStyle } from 'react-native';
3
+ import Animated, {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
3
10
 
4
11
  import { lazyRequire } from '../utils/lazy-require';
5
12
 
6
- // Lazy-loaded dependencies for the SVG spinner.
7
- // Falls back to ActivityIndicator if react-native-svg or react-native-reanimated are not installed.
13
+ // react-native-svg is loaded lazily so the loading module can fall back to
14
+ // ActivityIndicator when the host app doesn't ship SVG support. Reanimated,
15
+ // by contrast, MUST be statically imported: the worklets Babel plugin
16
+ // performs build-time closure analysis (`__closure` metadata) that fails on
17
+ // runtime requires, which would crash the UI thread with
18
+ // "Tried to synchronously call a non-worklet function `addListener`".
8
19
  type SvgModuleType = typeof import('react-native-svg');
9
- type ReanimatedType = typeof import('react-native-reanimated');
10
20
 
11
21
  const getSvgModule = lazyRequire<SvgModuleType>('react-native-svg');
12
- const getReanimated = lazyRequire<ReanimatedType>('react-native-reanimated');
13
22
 
14
23
  interface SpinnerIconProps {
15
24
  size?: number;
@@ -21,12 +30,11 @@ interface SpinnerIconProps {
21
30
  type AnimatedSpinnerProps = Omit<SpinnerIconProps, 'className'> & {
22
31
  className?: string;
23
32
  svg: NonNullable<SvgModuleType>;
24
- reanimated: NonNullable<ReanimatedType>;
25
33
  };
26
34
 
27
35
  /**
28
36
  * Inner component that unconditionally calls Reanimated hooks.
29
- * Only rendered when both react-native-svg and react-native-reanimated are available.
37
+ * Only rendered when react-native-svg is available.
30
38
  */
31
39
  const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
32
40
  color = 'currentColor',
@@ -34,17 +42,8 @@ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
34
42
  className,
35
43
  style,
36
44
  svg,
37
- reanimated,
38
45
  }) => {
39
46
  const { default: Svg, Rect } = svg;
40
- const {
41
- default: Animated,
42
- useAnimatedStyle,
43
- useSharedValue,
44
- withRepeat,
45
- withTiming,
46
- Easing,
47
- } = reanimated;
48
47
 
49
48
  const rotation = useSharedValue(0);
50
49
 
@@ -54,8 +53,8 @@ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
54
53
  -1,
55
54
  false,
56
55
  );
57
- // Reanimated shared values are stable references; withRepeat/withTiming/Easing
58
- // are module-level functions from the lazily-loaded module and are stable too.
56
+ // rotation is a stable shared value reference; withRepeat/withTiming/Easing
57
+ // are module-level constants from a static import and are stable too.
59
58
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
59
  }, []);
61
60
 
@@ -97,8 +96,8 @@ const AnimatedSpinner: React.FC<AnimatedSpinnerProps> = ({
97
96
 
98
97
  /**
99
98
  * iOS-style SVG spinner with 8 rotating rectangles and an opacity gradient trail.
100
- * Requires react-native-svg and react-native-reanimated as peer dependencies.
101
- * Falls back to ActivityIndicator if either is missing.
99
+ * Requires react-native-svg (lazy) and react-native-reanimated (static).
100
+ * Falls back to ActivityIndicator if react-native-svg is missing.
102
101
  */
103
102
  export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
104
103
  color = 'currentColor',
@@ -107,9 +106,8 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
107
106
  style,
108
107
  }) => {
109
108
  const svg = getSvgModule();
110
- const reanimated = getReanimated();
111
109
 
112
- if (!svg || !reanimated) {
110
+ if (!svg) {
113
111
  return <ActivityIndicator size={size > 30 ? 'large' : 'small'} color={color} />;
114
112
  }
115
113
 
@@ -120,7 +118,6 @@ export const SpinnerIcon: React.FC<SpinnerIconProps> = ({
120
118
  className={className}
121
119
  style={style}
122
120
  svg={svg}
123
- reanimated={reanimated}
124
121
  />
125
122
  );
126
123
  };
@@ -7,12 +7,20 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
- import { Platform, type StyleProp, Text, type TextStyle, View, type ViewStyle } from 'react-native';
10
+ import {
11
+ Platform,
12
+ Pressable,
13
+ type PressableProps,
14
+ type StyleProp,
15
+ Text,
16
+ type TextStyle,
17
+ View,
18
+ type ViewStyle,
19
+ } from 'react-native';
11
20
  import Animated, { Easing, LinearTransition } from 'react-native-reanimated';
12
21
 
13
22
  import { useTheme } from '../theme/use-theme';
14
23
  import { atoms as a, platform } from '../styles';
15
- import { Button, type ButtonProps } from '../button';
16
24
 
17
25
  const InternalContext = createContext<{
18
26
  type: 'tabs' | 'radio';
@@ -98,17 +106,27 @@ export function Root<T extends string>({
98
106
  };
99
107
  }, [value, selectedPosition, setSelectedPosition, onChange, type, size]);
100
108
 
109
+ // Height of the wrapping pill matches the active item height (item
110
+ // `minHeight` + 4px outer `p_xs` padding on both sides). Locking the
111
+ // outer View to this exact height keeps the control as a tight inline
112
+ // pill on every platform — without it, a parent column flex context
113
+ // (the default on a `<View>`) would let the Root stretch vertically
114
+ // and the items inside would inherit that stretched height, blowing
115
+ // the control up into a giant block on native.
116
+ const itemMinHeight = size === 'large' ? 40 : 32;
117
+ const pillHeight = itemMinHeight + 8;
118
+
101
119
  return (
102
120
  <View
103
121
  accessibilityLabel={label}
104
122
  accessibilityHint={accessibilityHint ?? ''}
105
123
  style={[
106
124
  a.w_full,
107
- a.flex_1,
108
125
  a.relative,
109
126
  a.flex_row,
127
+ a.align_center,
110
128
  { backgroundColor: theme.colors.contrast50 },
111
- { borderRadius: 14 },
129
+ { borderRadius: 14, height: pillHeight },
112
130
  a.p_xs,
113
131
  style,
114
132
  ]}
@@ -132,8 +150,20 @@ export function Item({
132
150
  style,
133
151
  children,
134
152
  onPress: onPressProp,
135
- ...props
136
- }: { value: string; children: React.ReactNode } & Omit<ButtonProps, 'children'>) {
153
+ accessibilityLabel,
154
+ accessibilityHint,
155
+ testID,
156
+ disabled,
157
+ }: {
158
+ value: string;
159
+ children: React.ReactNode;
160
+ style?: StyleProp<ViewStyle>;
161
+ onPress?: () => void;
162
+ accessibilityLabel?: string;
163
+ accessibilityHint?: string;
164
+ testID?: string;
165
+ disabled?: PressableProps['disabled'];
166
+ }) {
137
167
  const [position, setPosition] = useState<{ x: number; width: number } | null>(
138
168
  null,
139
169
  );
@@ -171,9 +201,23 @@ export function Item({
171
201
  onPressProp?.();
172
202
  }, [ctx, value, position, onPressProp]);
173
203
 
204
+ // We render the segment as a flat `Pressable` (not Bloom's `Button`)
205
+ // for two reasons:
206
+ // 1. Layout: we need the touch target to participate directly in the
207
+ // Root's row flex layout so `flex: 1` distributes the items
208
+ // evenly on every platform. Bloom Button wraps its Pressable in
209
+ // an Animated.View that doesn't forward layout-affecting styles,
210
+ // which would collapse the segment to its natural text width.
211
+ // 2. Semantics: the Root carries `role="tablist"`/`"radiogroup"` and
212
+ // each item carries `role="tab"`/`"radio"`. Bloom Button always
213
+ // adds `accessibilityRole="button"` — that overrides the correct
214
+ // a11y role for tablist children.
215
+ const itemRole = ctx.type === 'tabs' ? 'tab' : 'radio';
216
+ const itemMinHeight = ctx.size === 'large' ? 40 : 32;
217
+
174
218
  return (
175
219
  <View
176
- style={[a.flex_1, a.flex_row]}
220
+ style={[a.flex_1, a.flex_row, a.align_stretch]}
177
221
  onLayout={evt => {
178
222
  const measuredPosition = {
179
223
  x: evt.nativeEvent.layout.x,
@@ -184,23 +228,30 @@ export function Item({
184
228
  }
185
229
  setPosition(measuredPosition);
186
230
  }}>
187
- <Button
188
- {...props}
231
+ <Pressable
189
232
  onPress={onPress}
190
- accessibilityLabel={props.accessibilityLabel}
191
- accessibilityHint={props.accessibilityHint}
192
- style={[
233
+ accessibilityLabel={accessibilityLabel}
234
+ accessibilityHint={accessibilityHint}
235
+ accessibilityState={{ selected: active, disabled: !!disabled }}
236
+ role={itemRole}
237
+ disabled={disabled}
238
+ testID={testID}
239
+ style={({ pressed }) => [
193
240
  a.flex_1,
241
+ a.flex_row,
242
+ a.align_center,
243
+ a.justify_center,
194
244
  a.bg_transparent,
195
245
  a.px_sm,
196
246
  a.py_xs,
197
- { minHeight: ctx.size === 'large' ? 40 : 32, borderRadius: 10 },
247
+ { minHeight: itemMinHeight, borderRadius: 10 },
248
+ pressed && !disabled && { opacity: 0.7 },
198
249
  style,
199
250
  ]}>
200
251
  <InternalItemContext.Provider value={{ active }}>
201
252
  {children}
202
253
  </InternalItemContext.Provider>
203
- </Button>
254
+ </Pressable>
204
255
  </View>
205
256
  );
206
257
  }