react-native-molecules 0.5.0-beta.1 → 0.5.0-beta.10

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 (152) hide show
  1. package/README.md +87 -0
  2. package/components/Accordion/index.tsx +1 -6
  3. package/components/Accordion/utils.ts +17 -14
  4. package/components/ActivityIndicator/ActivityIndicator.tsx +12 -20
  5. package/components/ActivityIndicator/index.tsx +1 -5
  6. package/components/Appbar/index.tsx +1 -4
  7. package/components/Appbar/utils.ts +33 -21
  8. package/components/Avatar/index.tsx +1 -5
  9. package/components/Avatar/utils.ts +2 -6
  10. package/components/Backdrop/Backdrop.tsx +2 -2
  11. package/components/Backdrop/index.tsx +1 -5
  12. package/components/Backdrop/utils.ts +5 -6
  13. package/components/Badge/index.tsx +1 -5
  14. package/components/Badge/utils.ts +2 -6
  15. package/components/Button/Button.tsx +211 -264
  16. package/components/Button/index.tsx +9 -7
  17. package/components/Button/types.ts +16 -2
  18. package/components/Button/utils.ts +231 -210
  19. package/components/Card/Card.tsx +8 -4
  20. package/components/Card/CardContent.tsx +5 -4
  21. package/components/Card/CardHeader.tsx +5 -3
  22. package/components/Card/CardMedia.tsx +5 -3
  23. package/components/Card/CardTypography.tsx +5 -3
  24. package/components/Card/index.tsx +1 -5
  25. package/components/Card/utils.ts +5 -6
  26. package/components/Checkbox/Checkbox.tsx +1 -0
  27. package/components/Checkbox/CheckboxBase.ios.tsx +1 -0
  28. package/components/Checkbox/CheckboxBase.tsx +1 -0
  29. package/components/Checkbox/index.tsx +1 -5
  30. package/components/Checkbox/utils.ts +6 -6
  31. package/components/Chip/Chip.tsx +40 -52
  32. package/components/Chip/index.tsx +1 -5
  33. package/components/Chip/utils.ts +5 -13
  34. package/components/DatePickerDocked/index.tsx +1 -5
  35. package/components/DatePickerDocked/utils.ts +21 -19
  36. package/components/DatePickerInline/index.tsx +1 -5
  37. package/components/DatePickerInline/utils.ts +41 -28
  38. package/components/DatePickerInput/index.tsx +1 -5
  39. package/components/DatePickerInput/utils.ts +5 -6
  40. package/components/DatePickerModal/DatePickerModalHeader.tsx +1 -1
  41. package/components/DatePickerModal/index.tsx +1 -5
  42. package/components/DatePickerModal/utils.ts +17 -16
  43. package/components/DateTimePicker/index.tsx +1 -5
  44. package/components/DateTimePicker/utils.ts +5 -6
  45. package/components/Dialog/index.tsx +1 -5
  46. package/components/Dialog/utils.ts +22 -16
  47. package/components/Drawer/Collapsible/utils.ts +13 -13
  48. package/components/Drawer/Drawer.tsx +2 -3
  49. package/components/Drawer/DrawerContent.tsx +5 -3
  50. package/components/Drawer/DrawerFooter.tsx +5 -4
  51. package/components/Drawer/DrawerHeader.tsx +5 -4
  52. package/components/Drawer/DrawerItem.tsx +5 -3
  53. package/components/Drawer/DrawerItemGroup.tsx +5 -4
  54. package/components/Drawer/index.tsx +1 -5
  55. package/components/Drawer/utils.ts +7 -7
  56. package/components/ElementGroup/ElementGroup.tsx +16 -14
  57. package/components/ElementGroup/index.tsx +1 -5
  58. package/components/ElementGroup/utils.ts +5 -6
  59. package/components/FAB/index.tsx +1 -5
  60. package/components/FAB/utils.ts +2 -6
  61. package/components/FilePicker/index.tsx +1 -5
  62. package/components/FilePicker/utils.ts +5 -6
  63. package/components/HelperText/index.tsx +1 -5
  64. package/components/HelperText/utils.ts +5 -7
  65. package/components/HorizontalDivider/HorizontalDivider.tsx +5 -3
  66. package/components/HorizontalDivider/index.tsx +1 -5
  67. package/components/Icon/CrossFadeIcon.tsx +3 -5
  68. package/components/Icon/Icon.tsx +2 -4
  69. package/components/Icon/iconFactory.tsx +3 -3
  70. package/components/Icon/index.tsx +2 -6
  71. package/components/Icon/types.ts +17 -6
  72. package/components/IconButton/IconButton.tsx +45 -58
  73. package/components/IconButton/index.tsx +1 -5
  74. package/components/IconButton/utils.ts +15 -26
  75. package/components/If/index.tsx +1 -5
  76. package/components/InputAddon/index.tsx +1 -5
  77. package/components/InputAddon/utils.ts +5 -6
  78. package/components/Link/index.tsx +1 -5
  79. package/components/Link/utils.ts +2 -6
  80. package/components/ListItem/index.tsx +1 -5
  81. package/components/ListItem/utils.ts +13 -11
  82. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  83. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  84. package/components/LoadingIndicator/index.tsx +13 -0
  85. package/components/LoadingIndicator/utils.ts +117 -0
  86. package/components/Menu/index.tsx +1 -5
  87. package/components/Menu/utils.ts +6 -8
  88. package/components/Modal/index.tsx +1 -5
  89. package/components/Modal/utils.ts +2 -6
  90. package/components/NavigationRail/NavigationRailHeader.tsx +1 -1
  91. package/components/NavigationRail/index.tsx +1 -5
  92. package/components/NavigationRail/utils.ts +21 -17
  93. package/components/NavigationStack/index.tsx +1 -5
  94. package/components/NavigationStack/utils.tsx +7 -1
  95. package/components/Portal/index.tsx +1 -5
  96. package/components/RadioButton/index.ts +1 -5
  97. package/components/RadioButton/utils.ts +9 -8
  98. package/components/Rating/index.tsx +1 -5
  99. package/components/Rating/utils.ts +6 -8
  100. package/components/Select/Select.tsx +360 -501
  101. package/components/Select/index.ts +7 -14
  102. package/components/Select/types.ts +2 -4
  103. package/components/Select/utils.ts +215 -0
  104. package/components/Slot/Slot.tsx +244 -0
  105. package/components/Slot/compose-refs.tsx +60 -0
  106. package/components/Slot/index.tsx +8 -0
  107. package/components/StateLayer/index.tsx +1 -5
  108. package/components/StateLayer/utils.ts +5 -6
  109. package/components/Surface/Surface.android.tsx +34 -8
  110. package/components/Surface/Surface.ios.tsx +36 -29
  111. package/components/Surface/Surface.tsx +31 -4
  112. package/components/Surface/index.tsx +1 -5
  113. package/components/Surface/utils.ts +49 -36
  114. package/components/Switch/Switch.tsx +8 -2
  115. package/components/Switch/index.tsx +1 -5
  116. package/components/Switch/utils.ts +2 -6
  117. package/components/Tabs/index.tsx +1 -5
  118. package/components/Tabs/utils.ts +10 -10
  119. package/components/Text/Text.tsx +2 -8
  120. package/components/TextInput/TextInput.tsx +5 -4
  121. package/components/TextInput/index.tsx +1 -5
  122. package/components/TextInput/utils.ts +8 -15
  123. package/components/TextInputWithMask/index.tsx +1 -5
  124. package/components/TimePicker/AmPmSwitcher.tsx +1 -1
  125. package/components/TimePicker/index.tsx +1 -5
  126. package/components/TimePicker/utils.ts +29 -21
  127. package/components/TimePickerField/index.tsx +1 -5
  128. package/components/TimePickerField/utils.ts +5 -6
  129. package/components/TimePickerModal/TimePickerModal.tsx +6 -2
  130. package/components/TimePickerModal/index.tsx +1 -5
  131. package/components/TimePickerModal/utils.ts +5 -6
  132. package/components/Tooltip/TooltipTrigger.tsx +25 -16
  133. package/components/Tooltip/index.tsx +1 -5
  134. package/components/Tooltip/utils.ts +5 -6
  135. package/components/TouchableRipple/TouchableRipple.native.tsx +49 -13
  136. package/components/TouchableRipple/TouchableRipple.tsx +136 -46
  137. package/components/TouchableRipple/index.tsx +1 -5
  138. package/components/TouchableRipple/utils.ts +5 -6
  139. package/components/VerticalDivider/VerticalDivider.tsx +9 -8
  140. package/components/VerticalDivider/index.tsx +1 -5
  141. package/core/componentsRegistry.ts +31 -19
  142. package/hocs/withPortal.tsx +1 -1
  143. package/hooks/useControlledValue.tsx +20 -4
  144. package/hooks/useSubcomponents.tsx +56 -22
  145. package/hooks/useWhatHasUpdated.tsx +48 -0
  146. package/package.json +10 -13
  147. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +5 -2
  148. package/styles/shadow.ts +2 -1
  149. package/styles/themes/LightTheme.tsx +1 -1
  150. package/utils/extractPropertiesFromStyles.ts +25 -0
  151. package/utils/lodash.ts +77 -6
  152. package/utils/repository.ts +2 -52
@@ -5,15 +5,13 @@ import {
5
5
  type StyleProp,
6
6
  type TextStyle,
7
7
  type ViewProps,
8
- type ViewStyle,
9
8
  } from 'react-native';
10
9
 
11
10
  import { useActionState } from '../../hooks/useActionState';
12
11
  import { resolveStateVariant } from '../../utils';
13
- import { Icon, type IconType } from '../Icon';
12
+ import { Icon, type IconProps, type IconType } from '../Icon';
14
13
  import CrossFadeIcon from '../Icon/CrossFadeIcon';
15
14
  import { StateLayer } from '../StateLayer';
16
- import { Surface } from '../Surface';
17
15
  import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple';
18
16
  import type { IconButtonVariant } from './types';
19
17
  import { defaultStyles, iconButtonSizeToIconSizeMap } from './utils';
@@ -63,14 +61,11 @@ export type Props = Omit<TouchableRippleProps, 'children' | 'style'> & {
63
61
  */
64
62
  style?: StyleProp<TextStyle>;
65
63
  iconStyle?: StyleProp<TextStyle>;
64
+ iconProps?: Omit<IconProps, 'name' | 'type' | 'style' | 'color' | 'size'>;
66
65
  /**
67
66
  * color of the icon
68
67
  */
69
68
  color?: string;
70
- /**
71
- * Style of the innerContainer
72
- */
73
- innerContainerStyle?: ViewStyle;
74
69
  /**
75
70
  * Props for the state layer
76
71
  * */
@@ -92,10 +87,10 @@ const IconButton = (
92
87
  animated = false,
93
88
  variant = 'default',
94
89
  style,
95
- innerContainerStyle: innerContainerStyleProp = emptyObject,
96
90
  testID,
97
91
  stateLayerProps = emptyObject,
98
- iconStyle,
92
+ iconStyle: iconStyleProp,
93
+ iconProps,
99
94
  ...rest
100
95
  }: Props,
101
96
  ref: any,
@@ -113,7 +108,9 @@ const IconButton = (
113
108
  });
114
109
 
115
110
  defaultStyles.useVariants({
116
- variant,
111
+ // @ts-ignore // TODO - fix this
112
+ variant: variant as any,
113
+ // @ts-ignore // TODO - fix this
117
114
  state,
118
115
  size: typeof size === 'string' && size ? size : undefined,
119
116
  });
@@ -124,9 +121,9 @@ const IconButton = (
124
121
  rippleColor,
125
122
  containerStyle,
126
123
  accessibilityState,
127
- innerContainerStyle,
128
124
  // accessibilityTraits,
129
125
  stateLayerStyle,
126
+ iconStyle,
130
127
  } = useMemo(() => {
131
128
  const iconSizeInNum =
132
129
  iconButtonSizeToIconSizeMap[size as keyof typeof iconButtonSizeToIconSizeMap] ??
@@ -144,7 +141,6 @@ const IconButton = (
144
141
  iconColor: _iconColor,
145
142
  iconSize: iconSizeInNum,
146
143
  rippleColor: _rippleColor,
147
- innerContainerStyle: [defaultStyles.innerContainer, innerContainerStyleProp],
148
144
  containerStyle: [
149
145
  iconSizeInNum
150
146
  ? {
@@ -155,60 +151,51 @@ const IconButton = (
155
151
  defaultStyles.root,
156
152
  style,
157
153
  ],
154
+ iconStyle: [defaultStyles.icon, iconStyleProp],
158
155
  // accessibilityTraits: disabled ? ['button', 'disabled'] : 'button',
159
156
  accessibilityState: { disabled },
160
157
  stateLayerStyle: [defaultStyles.stateLayer, stateLayerProps?.style],
161
158
  };
162
159
  // eslint-disable-next-line react-hooks/exhaustive-deps
163
- }, [
164
- _iconColor,
165
- disabled,
166
- innerContainerStyleProp,
167
- size,
168
- stateLayerProps?.style,
169
- style,
170
- state,
171
- variant,
172
- ]);
160
+ }, [_iconColor, disabled, size, stateLayerProps?.style, style, state, variant]);
173
161
 
174
162
  return (
175
- <Surface style={containerStyle} elevation={0}>
176
- <TouchableRipple
177
- borderless
178
- centered
179
- onPress={onPress}
180
- rippleColor={rippleColor}
181
- accessibilityLabel={accessibilityLabel}
182
- style={innerContainerStyle}
183
- // accessibilityTraits={accessibilityTraits}
184
- // accessibilityComponentType="button"
185
- accessibilityRole="button"
186
- accessibilityState={accessibilityState}
187
- disabled={disabled}
188
- hitSlop={
189
- // @ts-ignore
190
- TouchableRipple?.supported ? rippleSupportedHitSlop : rippleUnsupportedHitSlop
191
- }
163
+ <TouchableRipple
164
+ borderless
165
+ centered
166
+ onPress={onPress}
167
+ rippleColor={rippleColor}
168
+ accessibilityLabel={accessibilityLabel}
169
+ style={containerStyle}
170
+ // accessibilityTraits={accessibilityTraits}
171
+ // accessibilityComponentType="button"
172
+ accessibilityRole="button"
173
+ accessibilityState={accessibilityState}
174
+ disabled={disabled}
175
+ hitSlop={
192
176
  // @ts-ignore
193
- ref={actionsRef}
194
- testID={testID}
195
- {...rest}>
196
- <>
197
- <IconComponent
198
- color={iconColor}
199
- name={name}
200
- size={iconSize}
201
- type={type}
202
- style={iconStyle}
203
- />
204
- <StateLayer
205
- testID={testID ? `${testID}-stateLayer` : ''}
206
- {...stateLayerProps}
207
- style={stateLayerStyle}
208
- />
209
- </>
210
- </TouchableRipple>
211
- </Surface>
177
+ TouchableRipple?.supported ? rippleSupportedHitSlop : rippleUnsupportedHitSlop
178
+ }
179
+ // @ts-ignore
180
+ ref={actionsRef}
181
+ testID={testID}
182
+ {...rest}>
183
+ <>
184
+ <IconComponent
185
+ color={iconColor}
186
+ name={name}
187
+ size={iconSize}
188
+ type={type}
189
+ style={iconStyle}
190
+ {...iconProps}
191
+ />
192
+ <StateLayer
193
+ testID={testID ? `${testID}-stateLayer` : ''}
194
+ {...stateLayerProps}
195
+ style={stateLayerStyle}
196
+ />
197
+ </>
198
+ </TouchableRipple>
212
199
  );
213
200
  };
214
201
 
@@ -1,10 +1,6 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import IconButtonDefault from './IconButton';
3
3
 
4
- registerMoleculesComponents({
5
- IconButton: IconButtonDefault,
6
- });
7
-
8
4
  export const IconButton = getRegisteredComponentWithFallback('IconButton', IconButtonDefault);
9
5
 
10
6
  export type { Props as IconButtonProps } from './IconButton';
@@ -1,11 +1,9 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
3
  import {
4
- getRegisteredMoleculesComponentStyles,
5
- registerComponentsStyles,
6
- registerComponentUtils,
7
- } from '../../core';
8
- import { getRegisteredComponentUtils } from './../../core/componentsRegistry';
4
+ getRegisteredComponentStylesWithFallback,
5
+ getRegisteredComponentUtilsWithFallback,
6
+ } from './../../core/componentsRegistry';
9
7
 
10
8
  export type States =
11
9
  | 'selectedAndDisabled'
@@ -29,6 +27,8 @@ const iconButtonStylesDefault = StyleSheet.create(theme => ({
29
27
  overflow: 'hidden',
30
28
  borderWidth: 0,
31
29
  backgroundColor: 'transparent',
30
+ justifyContent: 'center',
31
+ alignItems: 'center',
32
32
 
33
33
  variants: {
34
34
  size: {
@@ -215,11 +215,7 @@ const iconButtonStylesDefault = StyleSheet.create(theme => ({
215
215
  {
216
216
  variant: 'outlined',
217
217
  state: 'hovered',
218
- styles: {
219
- backgroundColor: theme.colors.inverseSurface,
220
- color: theme.colors.inverseOnSurface,
221
- borderWidth: 0,
222
- },
218
+ styles: {},
223
219
  },
224
220
  ],
225
221
  },
@@ -304,22 +300,15 @@ const iconButtonStylesDefault = StyleSheet.create(theme => ({
304
300
  ],
305
301
  },
306
302
 
307
- innerContainer: {
308
- flexGrow: 1,
309
- justifyContent: 'center',
310
- alignItems: 'center',
311
- },
303
+ icon: {},
312
304
  }));
313
305
 
314
- registerComponentsStyles({
315
- IconButton: iconButtonStylesDefault,
316
- });
317
-
318
- registerComponentUtils('IconButton', {
306
+ export const defaultStyles = getRegisteredComponentStylesWithFallback(
307
+ 'IconButton',
308
+ iconButtonStylesDefault,
309
+ );
310
+ export const iconButtonSizeToIconSizeMap = getRegisteredComponentUtilsWithFallback(
311
+ 'IconButton',
319
312
  iconButtonSizeToIconSizeMapDefault,
320
- });
321
-
322
- export const defaultStyles = getRegisteredMoleculesComponentStyles('IconButton');
323
- export const iconButtonSizeToIconSizeMap =
324
- getRegisteredComponentUtils('IconButton')?.iconButtonSizeToIconSizeMapDefault ||
325
- iconButtonSizeToIconSizeMapDefault;
313
+ 'iconButtonSizeToIconSizeMap',
314
+ );
@@ -1,13 +1,9 @@
1
1
  import type { PropsWithChildren } from 'react';
2
2
 
3
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
3
+ import { getRegisteredComponentWithFallback } from '../../core';
4
4
 
5
5
  const IfDefault = (props: PropsWithChildren<{ shouldRender?: boolean }>) => {
6
6
  return <>{!!props.shouldRender && props.children}</>;
7
7
  };
8
8
 
9
- registerMoleculesComponents({
10
- If: IfDefault,
11
- });
12
-
13
9
  export const If = getRegisteredComponentWithFallback('If', IfDefault);
@@ -1,10 +1,6 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import InputAddonDefault from './InputAddon';
3
3
 
4
- registerMoleculesComponents({
5
- InputAddon: InputAddonDefault,
6
- });
7
-
8
4
  export const InputAddon = getRegisteredComponentWithFallback('InputAddon', InputAddonDefault);
9
5
 
10
6
  export type { Props as InputAddonProps } from './InputAddon';
@@ -1,6 +1,6 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
- import { getRegisteredMoleculesComponentStyles, registerComponentsStyles } from '../../core';
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
4
 
5
5
  const inputAddonStylesDefault = StyleSheet.create(theme => ({
6
6
  root: {
@@ -26,8 +26,7 @@ const inputAddonStylesDefault = StyleSheet.create(theme => ({
26
26
  },
27
27
  }));
28
28
 
29
- registerComponentsStyles({
30
- InputAddon: inputAddonStylesDefault,
31
- });
32
-
33
- export const inputAddonStyles = getRegisteredMoleculesComponentStyles('InputAddon');
29
+ export const inputAddonStyles = getRegisteredComponentStylesWithFallback(
30
+ 'InputAddon',
31
+ inputAddonStylesDefault,
32
+ );
@@ -1,10 +1,6 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import LinkDefault from './Link';
3
3
 
4
- registerMoleculesComponents({
5
- Link: LinkDefault,
6
- });
7
-
8
4
  export const Link = getRegisteredComponentWithFallback('Link', LinkDefault);
9
5
 
10
6
  export type { Props as LinkProps } from './Link';
@@ -1,6 +1,6 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
- import { getRegisteredMoleculesComponentStyles, registerComponentsStyles } from '../../core';
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
4
 
5
5
  // type States = 'disabled' | 'hovered';
6
6
 
@@ -32,8 +32,4 @@ const linkStylesDefault = StyleSheet.create(theme => ({
32
32
  },
33
33
  }));
34
34
 
35
- registerComponentsStyles({
36
- Link: linkStylesDefault,
37
- });
38
-
39
- export const linkStyles = getRegisteredMoleculesComponentStyles('Link');
35
+ export const linkStyles = getRegisteredComponentStylesWithFallback('Link', linkStylesDefault);
@@ -1,4 +1,4 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import ListItemComponent from './ListItem';
3
3
  import ListItemDescription from './ListItemDescription';
4
4
  import ListItemTitle from './ListItemTitle';
@@ -8,10 +8,6 @@ const ListItemDefault = Object.assign(ListItemComponent, {
8
8
  Description: ListItemDescription,
9
9
  });
10
10
 
11
- registerMoleculesComponents({
12
- ListItem: ListItemDefault,
13
- });
14
-
15
11
  export const ListItem = getRegisteredComponentWithFallback('ListItem', ListItemDefault);
16
12
 
17
13
  export type { Props as ListItemProps } from './ListItem';
@@ -1,6 +1,6 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
- import { getRegisteredMoleculesComponentStyles, registerComponentsStyles } from '../../core';
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
4
 
5
5
  const listItemStylesDefault = StyleSheet.create(theme => ({
6
6
  root: {
@@ -101,13 +101,15 @@ const listItemDescriptionStylesDefault = StyleSheet.create(theme => ({
101
101
  },
102
102
  }));
103
103
 
104
- registerComponentsStyles({
105
- ListItem: listItemStylesDefault,
106
- ListItem_Title: listItemTitleStylesDefault,
107
- ListItem_Description: listItemDescriptionStylesDefault,
108
- });
109
-
110
- export const listItemStyles = getRegisteredMoleculesComponentStyles('ListItem');
111
- export const listItemTitleStyles = getRegisteredMoleculesComponentStyles('ListItem_Title');
112
- export const listItemDescriptionStyles =
113
- getRegisteredMoleculesComponentStyles('ListItem_Description');
104
+ export const listItemStyles = getRegisteredComponentStylesWithFallback(
105
+ 'ListItem',
106
+ listItemStylesDefault,
107
+ );
108
+ export const listItemTitleStyles = getRegisteredComponentStylesWithFallback(
109
+ 'ListItem_Title',
110
+ listItemTitleStylesDefault,
111
+ );
112
+ export const listItemDescriptionStyles = getRegisteredComponentStylesWithFallback(
113
+ 'ListItem_Description',
114
+ listItemDescriptionStylesDefault,
115
+ );
@@ -0,0 +1,253 @@
1
+ import { interpolate as flubberInterpolate } from 'flubber';
2
+ import { memo, useCallback, useEffect, useState } from 'react';
3
+ import { View } from 'react-native';
4
+ import Animated, {
5
+ cancelAnimation,
6
+ Easing,
7
+ useAnimatedStyle,
8
+ useDerivedValue,
9
+ useFrameCallback,
10
+ useSharedValue,
11
+ withRepeat,
12
+ withTiming,
13
+ } from 'react-native-reanimated';
14
+ import Svg, { Path } from 'react-native-svg';
15
+
16
+ import {
17
+ cookie4Path,
18
+ cookie9Path,
19
+ loadingIndicatorStyles as componentStyles,
20
+ ovalPath,
21
+ pentagonPath,
22
+ pillPath,
23
+ type Props,
24
+ softBurstPath,
25
+ sunnyPath,
26
+ useProcessProps,
27
+ } from './utils';
28
+
29
+ // Animation constants matching the web version
30
+ const ANIMATION_DURATION = 4550; // 4.55 seconds total cycle
31
+ const PULSE_DURATION = 2275; // 2.275 seconds for pulse
32
+
33
+ const SHAPE_PATHS = [
34
+ softBurstPath,
35
+ cookie9Path,
36
+ pentagonPath,
37
+ pillPath,
38
+ sunnyPath,
39
+ cookie4Path,
40
+ ovalPath,
41
+ ];
42
+
43
+ // Number of pre-computed frames per transition for smooth animation
44
+ const FRAMES_PER_TRANSITION = 30;
45
+ const TOTAL_FRAMES = SHAPE_PATHS.length * FRAMES_PER_TRANSITION;
46
+
47
+ // Material 3 Express Fast Spatial easing function
48
+ // Original: cubic-bezier(0.42, 1.67, 0.21, 0.90)
49
+ const expressFastSpatialEase = (t: number): number => {
50
+ 'worklet';
51
+ // Approximate cubic-bezier(0.42, 1.67, 0.21, 0.90) using a simplified function
52
+ // This creates the characteristic overshoot and fast settle
53
+ const p1x = 0.42,
54
+ p1y = 1.67,
55
+ p2x = 0.21,
56
+ p2y = 0.9;
57
+
58
+ // Simple cubic bezier approximation
59
+ const cx = 3.0 * p1x;
60
+ const bx = 3.0 * (p2x - p1x) - cx;
61
+ const ax = 1.0 - cx - bx;
62
+
63
+ const cy = 3.0 * p1y;
64
+ const by = 3.0 * (p2y - p1y) - cy;
65
+ const ay = 1.0 - cy - by;
66
+
67
+ // Sample the bezier curve
68
+ const sampleX = (x: number) => ((ax * x + bx) * x + cx) * x;
69
+ const sampleY = (x: number) => ((ay * x + by) * x + cy) * x;
70
+
71
+ // Newton-Raphson to find t for given x
72
+ let guess = t;
73
+ for (let i = 0; i < 4; i++) {
74
+ const currentX = sampleX(guess) - t;
75
+ const currentSlope = (3.0 * ax * guess + 2.0 * bx) * guess + cx;
76
+ if (Math.abs(currentSlope) < 1e-6) break;
77
+ guess -= currentX / currentSlope;
78
+ }
79
+
80
+ return sampleY(guess);
81
+ };
82
+
83
+ /**
84
+ * Pre-compute all animation frames at initialization for smooth playback.
85
+ * This avoids runtime flubber calls which can cause jank.
86
+ */
87
+ const precomputeFrames = (): string[] => {
88
+ const frames: string[] = [];
89
+
90
+ for (let i = 0; i < SHAPE_PATHS.length; i++) {
91
+ const fromPath = SHAPE_PATHS[i];
92
+ const toPath = SHAPE_PATHS[(i + 1) % SHAPE_PATHS.length];
93
+
94
+ // Create interpolator with higher precision
95
+ const interpolator = flubberInterpolate(fromPath, toPath, {
96
+ maxSegmentLength: 1, // Higher precision for smoother morphing
97
+ });
98
+
99
+ for (let j = 0; j < FRAMES_PER_TRANSITION; j++) {
100
+ const t = j / FRAMES_PER_TRANSITION;
101
+ // Apply easing to match CSS animation
102
+ const easedT = expressFastSpatialEase(t);
103
+ frames.push(interpolator(Math.max(0, Math.min(1, easedT))));
104
+ }
105
+ }
106
+
107
+ return frames;
108
+ };
109
+
110
+ const frames = precomputeFrames();
111
+
112
+ const LoadingIndicator = ({
113
+ animating = true,
114
+ color: colorProp,
115
+ size: sizeProp = 'md',
116
+ style,
117
+ variant = 'default',
118
+ innerContainerProps,
119
+ ...rest
120
+ }: Props) => {
121
+ const [currentPath, setCurrentPath] = useState(frames[0]);
122
+
123
+ const progress = useSharedValue(0);
124
+ const pulseScale = useSharedValue(1);
125
+ // Track last frame to avoid redundant updates
126
+ const lastFrameRef = useSharedValue(-1);
127
+
128
+ componentStyles.useVariants({
129
+ variant: variant as 'contained',
130
+ });
131
+
132
+ const { size, strokeColor } = useProcessProps({
133
+ variant,
134
+ size: sizeProp,
135
+ color: colorProp,
136
+ });
137
+
138
+ const updatePathFromFrame = useCallback((frameIndex: number) => {
139
+ const safeIndex = Math.max(0, Math.min(frameIndex, frames.length - 1));
140
+ setCurrentPath(prevPath => {
141
+ const newPath = frames[safeIndex];
142
+ return prevPath === newPath ? prevPath : newPath;
143
+ });
144
+ }, []);
145
+
146
+ useFrameCallback(() => {
147
+ 'worklet';
148
+ const frameIndex = Math.floor(progress.value * TOTAL_FRAMES) % TOTAL_FRAMES;
149
+ if (frameIndex !== lastFrameRef.value) {
150
+ lastFrameRef.value = frameIndex;
151
+ updatePathFromFrame(frameIndex);
152
+ }
153
+ });
154
+
155
+ useEffect(() => {
156
+ if (animating) {
157
+ // Main morphing and rotation animation
158
+ progress.value = 0;
159
+ progress.value = withRepeat(
160
+ withTiming(1, {
161
+ duration: ANIMATION_DURATION,
162
+ easing: Easing.linear,
163
+ }),
164
+ -1, // Infinite repeat
165
+ false, // Don't reverse
166
+ );
167
+
168
+ // Pulse animation
169
+ pulseScale.value = 1;
170
+ pulseScale.value = withRepeat(
171
+ withTiming(1.1, {
172
+ duration: PULSE_DURATION,
173
+ easing: Easing.inOut(Easing.ease),
174
+ }),
175
+ -1, // Infinite repeat
176
+ true, // Reverse (ping-pong effect)
177
+ );
178
+ } else {
179
+ cancelAnimation(progress);
180
+ cancelAnimation(pulseScale);
181
+ progress.value = 0;
182
+ pulseScale.value = 1;
183
+ updatePathFromFrame(0);
184
+ }
185
+
186
+ return () => {
187
+ cancelAnimation(progress);
188
+ cancelAnimation(pulseScale);
189
+ };
190
+ }, [animating, progress, pulseScale, updatePathFromFrame]);
191
+
192
+ // Derived value for rotation with per-segment easing (matches CSS animation-timing-function per keyframe)
193
+ const rotation = useDerivedValue(() => {
194
+ 'worklet';
195
+ const p = progress.value;
196
+ const segmentCount = SHAPE_PATHS.length;
197
+
198
+ // Determine which segment we're in
199
+ const scaledProgress = p * segmentCount;
200
+ const segmentIndex = Math.min(Math.floor(scaledProgress), segmentCount - 1);
201
+ const segmentProgress = scaledProgress - segmentIndex;
202
+
203
+ // Apply easing to this segment's progress
204
+ const easedSegmentProgress = expressFastSpatialEase(segmentProgress);
205
+
206
+ // Calculate rotation: base rotation for completed segments + eased progress within current segment
207
+ const rotationPerSegment = 1080 / segmentCount; // ~154.29 degrees per segment
208
+ const baseRotation = segmentIndex * rotationPerSegment;
209
+ const segmentRotation = easedSegmentProgress * rotationPerSegment;
210
+
211
+ return baseRotation + segmentRotation;
212
+ });
213
+
214
+ const animatedStyle = useAnimatedStyle(() => {
215
+ return {
216
+ transform: [{ scale: pulseScale.value }, { rotate: `${rotation.value}deg` }],
217
+ };
218
+ });
219
+
220
+ if (!animating) return null;
221
+
222
+ return (
223
+ <View
224
+ style={[
225
+ componentStyles.container,
226
+ {
227
+ width: Math.floor((10 / 48) * size * 2) + size,
228
+ height: Math.floor((10 / 48) * size * 2) + size,
229
+ },
230
+ style,
231
+ ]}
232
+ accessible
233
+ accessibilityLabel="Loading"
234
+ accessibilityRole="progressbar"
235
+ accessibilityState={{ busy: animating }}
236
+ {...rest}>
237
+ <Animated.View
238
+ {...innerContainerProps}
239
+ style={[
240
+ { width: size, height: size },
241
+ componentStyles.innerContainer,
242
+ innerContainerProps?.style,
243
+ animatedStyle,
244
+ ]}>
245
+ <Svg width={size} height={size} viewBox="0 0 38 38">
246
+ <Path d={currentPath} fill={strokeColor} />
247
+ </Svg>
248
+ </Animated.View>
249
+ </View>
250
+ );
251
+ };
252
+
253
+ export default memo(LoadingIndicator);