react-native-molecules 0.5.0-beta.3 → 0.5.0-beta.30

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 (227) hide show
  1. package/components/Accordion/Accordion.tsx +2 -6
  2. package/components/Accordion/AccordionItem.tsx +16 -12
  3. package/components/Accordion/AccordionItemContent.tsx +6 -1
  4. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  5. package/components/Accordion/utils.ts +6 -0
  6. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  7. package/components/Appbar/AppbarBase.tsx +18 -13
  8. package/components/Button/Button.tsx +211 -264
  9. package/components/Button/index.tsx +9 -3
  10. package/components/Button/types.ts +16 -2
  11. package/components/Button/utils.ts +230 -208
  12. package/components/Card/Card.tsx +1 -1
  13. package/components/Checkbox/Checkbox.tsx +125 -88
  14. package/components/Checkbox/CheckboxBase.ios.tsx +14 -23
  15. package/components/Checkbox/CheckboxBase.tsx +21 -137
  16. package/components/Checkbox/context.tsx +14 -0
  17. package/components/Checkbox/index.tsx +11 -4
  18. package/components/Checkbox/types.ts +63 -29
  19. package/components/Checkbox/utils.ts +25 -108
  20. package/components/Chip/Chip.tsx +41 -52
  21. package/components/Chip/utils.ts +3 -7
  22. package/components/DateField/DateField.tsx +111 -0
  23. package/components/DateField/index.tsx +6 -0
  24. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +19 -51
  25. package/components/DatePicker/DateCalendar.tsx +83 -0
  26. package/components/DatePicker/DatePickerActions.tsx +73 -0
  27. package/components/DatePicker/DatePickerModal.tsx +246 -0
  28. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  29. package/components/DatePicker/DatePickerProvider.tsx +158 -0
  30. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  31. package/components/DatePicker/context.tsx +83 -0
  32. package/components/DatePicker/index.tsx +45 -0
  33. package/components/DatePicker/utils.ts +295 -0
  34. package/components/DatePickerInline/DatePickerDockedHeader.tsx +117 -0
  35. package/components/DatePickerInline/DatePickerInline.tsx +17 -16
  36. package/components/DatePickerInline/DatePickerInlineBase.tsx +11 -5
  37. package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
  38. package/components/DatePickerInline/Day.tsx +25 -1
  39. package/components/DatePickerInline/DayNames.tsx +13 -10
  40. package/components/DatePickerInline/DayRange.tsx +2 -4
  41. package/components/DatePickerInline/HeaderItem.tsx +44 -29
  42. package/components/DatePickerInline/Month.tsx +48 -67
  43. package/components/DatePickerInline/MonthPicker.tsx +80 -92
  44. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  45. package/components/DatePickerInline/Swiper.tsx +169 -14
  46. package/components/DatePickerInline/SwiperUtils.ts +1 -1
  47. package/components/DatePickerInline/Week.tsx +6 -1
  48. package/components/DatePickerInline/YearPicker.tsx +220 -78
  49. package/components/DatePickerInline/dateUtils.tsx +18 -13
  50. package/components/DatePickerInline/store.tsx +27 -0
  51. package/components/DatePickerInline/types.ts +6 -2
  52. package/components/DatePickerInline/utils.ts +66 -29
  53. package/components/Divider/Divider.tsx +192 -0
  54. package/components/Divider/index.tsx +10 -0
  55. package/components/Drawer/Drawer.tsx +17 -6
  56. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  57. package/components/ElementGroup/ElementGroup.tsx +1 -1
  58. package/components/FilePicker/FilePicker.tsx +48 -78
  59. package/components/FilePicker/index.tsx +2 -1
  60. package/components/FilePicker/utils.ts +9 -0
  61. package/components/HelperText/HelperText.tsx +0 -35
  62. package/components/Icon/iconFactory.tsx +5 -4
  63. package/components/Icon/index.tsx +1 -1
  64. package/components/Icon/types.ts +17 -6
  65. package/components/IconButton/IconButton.tsx +84 -84
  66. package/components/IconButton/index.tsx +1 -0
  67. package/components/IconButton/types.ts +10 -0
  68. package/components/IconButton/utils.ts +167 -33
  69. package/components/List/List.tsx +276 -0
  70. package/components/List/context.tsx +27 -0
  71. package/components/List/index.ts +8 -0
  72. package/components/List/types.ts +117 -0
  73. package/components/List/utils.ts +79 -0
  74. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  75. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  76. package/components/LoadingIndicator/index.tsx +13 -0
  77. package/components/LoadingIndicator/utils.ts +117 -0
  78. package/components/Menu/Menu.tsx +162 -39
  79. package/components/Menu/index.tsx +10 -7
  80. package/components/Menu/utils.ts +21 -70
  81. package/components/NavigationRail/NavigationRail.tsx +15 -9
  82. package/components/Popover/Popover.tsx +119 -145
  83. package/components/Popover/PopoverRoot.tsx +60 -0
  84. package/components/Popover/common.ts +54 -34
  85. package/components/Popover/index.ts +12 -1
  86. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  87. package/components/Popover/usePlatformMeasure.ts +120 -0
  88. package/components/Popover/utils.ts +34 -0
  89. package/components/Portal/Portal.tsx +1 -2
  90. package/components/Radio/Radio.tsx +188 -0
  91. package/components/Radio/RadioBase.ios.tsx +69 -0
  92. package/components/Radio/RadioBase.tsx +136 -0
  93. package/components/Radio/context.tsx +23 -0
  94. package/components/Radio/index.tsx +20 -0
  95. package/components/Radio/types.ts +101 -0
  96. package/components/Radio/utils.ts +115 -0
  97. package/components/Rating/Rating.tsx +1 -1
  98. package/components/Select/Select.tsx +521 -785
  99. package/components/Select/context.tsx +81 -0
  100. package/components/Select/index.ts +26 -14
  101. package/components/Select/types.ts +65 -58
  102. package/components/Select/utils.ts +126 -0
  103. package/components/Slot/Slot.tsx +244 -0
  104. package/components/Slot/compose-refs.tsx +62 -0
  105. package/components/Slot/index.tsx +8 -0
  106. package/components/Surface/Surface.android.tsx +32 -7
  107. package/components/Surface/Surface.ios.tsx +34 -29
  108. package/components/Surface/Surface.tsx +31 -4
  109. package/components/Surface/utils.ts +44 -6
  110. package/components/Switch/Switch.ios.tsx +1 -1
  111. package/components/Switch/Switch.tsx +10 -3
  112. package/components/Tabs/TabItem.tsx +35 -58
  113. package/components/Tabs/TabLabel.tsx +5 -9
  114. package/components/Tabs/Tabs.tsx +156 -150
  115. package/components/Tabs/utils.ts +15 -2
  116. package/components/Text/textFactory.tsx +17 -5
  117. package/components/TextInput/TextInput.tsx +663 -579
  118. package/components/TextInput/index.tsx +19 -3
  119. package/components/TextInput/types.ts +77 -28
  120. package/components/TextInput/utils.ts +235 -145
  121. package/components/TimeField/TimeField.tsx +75 -0
  122. package/components/TimeField/index.tsx +6 -0
  123. package/components/TimeField/useTimeFieldState.ts +70 -0
  124. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  125. package/components/TimePicker/AnalogClock.tsx +1 -1
  126. package/components/TimePicker/TimeInput.tsx +87 -42
  127. package/components/TimePicker/TimeInputs.tsx +138 -50
  128. package/components/TimePicker/TimePicker.tsx +74 -11
  129. package/components/TimePicker/TimePickerModal.tsx +186 -0
  130. package/components/TimePicker/context.tsx +17 -0
  131. package/components/TimePicker/index.tsx +15 -3
  132. package/components/TimePicker/utils.ts +93 -4
  133. package/components/Tooltip/Tooltip.tsx +42 -67
  134. package/components/Tooltip/TooltipContent.tsx +32 -5
  135. package/components/Tooltip/TooltipTrigger.tsx +20 -20
  136. package/components/Tooltip/index.tsx +1 -1
  137. package/components/TouchableRipple/TouchableRipple.native.tsx +83 -16
  138. package/components/TouchableRipple/TouchableRipple.tsx +150 -102
  139. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  140. package/hocs/index.tsx +1 -1
  141. package/hocs/withKeyboardAccessibility.tsx +2 -3
  142. package/hocs/withPortal.tsx +1 -1
  143. package/hooks/index.tsx +2 -12
  144. package/hooks/useActionState.tsx +19 -8
  145. package/hooks/useContrastColor.ts +1 -2
  146. package/hooks/useFilePicker.tsx +7 -17
  147. package/hooks/useHandleNumberFormat.tsx +2 -2
  148. package/hooks/useMediaQuery.tsx +1 -2
  149. package/package.json +95 -111
  150. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +6 -3
  151. package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
  152. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
  153. package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
  154. package/styles/shadow.ts +2 -1
  155. package/styles/themes/LightTheme.tsx +1 -1
  156. package/utils/DocumentPicker/documentPicker.ts +78 -27
  157. package/utils/DocumentPicker/types.ts +0 -1
  158. package/utils/extractSubcomponents.ts +89 -0
  159. package/utils/extractTextStyles.ts +1 -2
  160. package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
  161. package/utils/index.ts +0 -3
  162. package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
  163. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  164. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  165. package/components/DatePickerDocked/index.tsx +0 -17
  166. package/components/DatePickerDocked/types.ts +0 -11
  167. package/components/DatePickerDocked/utils.ts +0 -157
  168. package/components/DatePickerInline/DatePickerContext.tsx +0 -21
  169. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  170. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  171. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  172. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  173. package/components/DatePickerInput/index.tsx +0 -10
  174. package/components/DatePickerInput/types.ts +0 -28
  175. package/components/DatePickerInput/utils.ts +0 -15
  176. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  177. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  178. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  179. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  180. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  181. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  182. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  183. package/components/DatePickerModal/index.tsx +0 -16
  184. package/components/DatePickerModal/types.ts +0 -92
  185. package/components/DatePickerModal/utils.ts +0 -122
  186. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  187. package/components/DateTimePicker/index.tsx +0 -10
  188. package/components/DateTimePicker/utils.ts +0 -12
  189. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  190. package/components/HorizontalDivider/index.tsx +0 -9
  191. package/components/ListItem/ListItem.tsx +0 -136
  192. package/components/ListItem/ListItemDescription.tsx +0 -25
  193. package/components/ListItem/ListItemTitle.tsx +0 -25
  194. package/components/ListItem/index.tsx +0 -14
  195. package/components/ListItem/utils.ts +0 -115
  196. package/components/Menu/MenuDivider.tsx +0 -13
  197. package/components/Menu/MenuItem.tsx +0 -128
  198. package/components/Popover/Popover.native.tsx +0 -185
  199. package/components/RadioButton/RadioButton.tsx +0 -138
  200. package/components/RadioButton/RadioButtonAndroid.tsx +0 -188
  201. package/components/RadioButton/RadioButtonGroup.tsx +0 -98
  202. package/components/RadioButton/RadioButtonIOS.tsx +0 -106
  203. package/components/RadioButton/RadioButtonItem.tsx +0 -232
  204. package/components/RadioButton/index.ts +0 -22
  205. package/components/RadioButton/utils.ts +0 -165
  206. package/components/TimePickerField/TimePickerField.tsx +0 -152
  207. package/components/TimePickerField/index.tsx +0 -10
  208. package/components/TimePickerField/utils.ts +0 -94
  209. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  210. package/components/TimePickerModal/index.tsx +0 -10
  211. package/components/TimePickerModal/utils.ts +0 -47
  212. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  213. package/components/VerticalDivider/index.tsx +0 -9
  214. package/context-bridge/index.tsx +0 -87
  215. package/fast-context/index.tsx +0 -190
  216. package/hocs/typedMemo.tsx +0 -5
  217. package/hooks/useControlledValue.tsx +0 -68
  218. package/hooks/useLatest.tsx +0 -9
  219. package/hooks/useMergedRefs.ts +0 -14
  220. package/hooks/usePrevious.ts +0 -13
  221. package/hooks/useSearchable.tsx +0 -74
  222. package/hooks/useSubcomponents.tsx +0 -59
  223. package/hooks/useToggle.tsx +0 -24
  224. package/utils/color.ts +0 -22
  225. package/utils/compare/index.ts +0 -54
  226. package/utils/lodash.ts +0 -49
  227. package/utils/repository.ts +0 -53
@@ -0,0 +1,117 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import {
3
+ type AccessibilityRole,
4
+ type GestureResponderEvent,
5
+ type ScrollViewProps,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+
10
+ import type { TouchableRippleProps } from '../TouchableRipple';
11
+
12
+ export type ListItemId = string | number;
13
+
14
+ export type DefaultListItemT = {
15
+ id?: ListItemId;
16
+ label?: string;
17
+ selectable?: boolean;
18
+ [key: string]: unknown;
19
+ };
20
+
21
+ export type ListValue<Multiple extends boolean = false> = Multiple extends true
22
+ ? ListItemId[]
23
+ : ListItemId | null;
24
+
25
+ export type ListEmptyStateRender = (ctx: {
26
+ /** True when `items` (the raw input) has at least one entry. */
27
+ hasItems: boolean;
28
+ }) => ReactNode;
29
+
30
+ export type ListContextValue<Option extends object = DefaultListItemT> = {
31
+ value: ListItemId | ListItemId[] | null;
32
+ multiple: boolean;
33
+ onAdd: (item: Option) => void;
34
+ onRemove: (item: Option) => void;
35
+ isSelectedId: (id: ListItemId) => boolean;
36
+ disabled?: boolean;
37
+ error: boolean;
38
+ allowDeselect: boolean;
39
+ };
40
+
41
+ type ListPropsBase = {
42
+ children: ReactNode;
43
+ disabled?: boolean;
44
+ error?: boolean;
45
+ /**
46
+ * Whether re-clicking the currently-selected row should remove it. Defaults
47
+ * to `true` for multiple, `false` for single (re-clicking the picked row
48
+ * in a "pick one and close" flow shouldn't clear the value).
49
+ */
50
+ allowDeselect?: boolean;
51
+ };
52
+
53
+ type SingleListProps<Option extends object = DefaultListItemT> = {
54
+ multiple?: false | undefined;
55
+ value?: ListValue<false>;
56
+ defaultValue?: ListValue<false>;
57
+ onChange?: (value: ListValue<false>, item: Option, event?: GestureResponderEvent) => void;
58
+ };
59
+
60
+ type MultipleListProps<Option extends object = DefaultListItemT> = {
61
+ multiple: true;
62
+ value?: ListValue<true>;
63
+ defaultValue?: ListValue<true>;
64
+ onChange?: (value: ListValue<true>, item: Option, event?: GestureResponderEvent) => void;
65
+ };
66
+
67
+ export type ListProps<Option extends object = DefaultListItemT> = ListPropsBase &
68
+ (SingleListProps<Option> | MultipleListProps<Option>);
69
+
70
+ export type ListContentProps = Omit<ScrollViewProps, 'children'> & {
71
+ children?: ReactNode;
72
+ };
73
+
74
+ /**
75
+ * Props for `<List.Item>`. When `value` is provided, the item participates in the
76
+ * surrounding `<List>` context — it derives its `selected` state from the context's
77
+ * value and toggles selection on press (unless `shouldToggleOnPress` is false).
78
+ *
79
+ * Without `value`, the item is a plain styled row (use it for menu-style entries
80
+ * that don't represent a selectable option).
81
+ *
82
+ * Note: when `value` is set, both `onPress` and the selection toggle fire on press,
83
+ * in that order. For most cases that's fine — pass `onPress` for side effects
84
+ * (e.g. closing a menu) and let the toggle drive `onChange`. Pass
85
+ * `onBeforeToggle` for side effects that should only run when the built-in
86
+ * toggle will happen. Set `shouldToggleOnPress={false}` to suppress the toggle entirely.
87
+ *
88
+ * Deselection: by default, single-select rows do **not** deselect on re-click
89
+ * (use `<List allowDeselect>` to opt in or out at the list level).
90
+ */
91
+ export type ListItemProps<Option extends object = DefaultListItemT> = Omit<
92
+ TouchableRippleProps,
93
+ 'children' | 'onPress'
94
+ > & {
95
+ ref?: RefObject<unknown>;
96
+ children?: ReactNode;
97
+ value?: ListItemId;
98
+ style?: StyleProp<ViewStyle>;
99
+ variant?: 'default' | 'menuItem';
100
+ selected?: boolean;
101
+ disabled?: boolean;
102
+ hovered?: boolean;
103
+ hoverable?: boolean;
104
+ shouldToggleOnPress?: boolean;
105
+ /** Runs after `onPress`, before the built-in selection toggle. */
106
+ onBeforeToggle?: (event: GestureResponderEvent) => void;
107
+ onPress?: (event: GestureResponderEvent) => void;
108
+ accessibilityRole?: AccessibilityRole;
109
+ accessibilityState?: Record<string, unknown>;
110
+ /** Reserved for generic item shape; not consumed directly. */
111
+ __optionType?: Option;
112
+ };
113
+
114
+ export type ListItemElementProps = {
115
+ children?: ReactNode;
116
+ style?: StyleProp<ViewStyle>;
117
+ };
@@ -0,0 +1,79 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
+
5
+ const defaultStyles = StyleSheet.create(theme => ({
6
+ emptyState: {
7
+ paddingHorizontal: theme.spacings['4'],
8
+ paddingVertical: theme.spacings['6'],
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ },
12
+ emptyStateText: {
13
+ color: theme.colors.onSurfaceVariant,
14
+ fontSize: 14,
15
+ },
16
+ }));
17
+
18
+ export const listStyles = getRegisteredComponentStylesWithFallback('List', defaultStyles);
19
+
20
+ const listItemStylesDefault = StyleSheet.create(theme => ({
21
+ root: {
22
+ backgroundColor: theme.colors.surface,
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ gap: theme.spacings['4'],
26
+
27
+ _web: {
28
+ outlineStyle: 'none',
29
+ },
30
+
31
+ variants: {
32
+ state: {
33
+ disabled: {
34
+ opacity: 0.38,
35
+ },
36
+ hovered: {},
37
+ focused: {},
38
+
39
+ selected: {
40
+ backgroundColor: theme.colors.surfaceVariant,
41
+ },
42
+ selectedAndFocused: {
43
+ backgroundColor: theme.colors.surfaceVariant,
44
+ },
45
+ },
46
+ variant: {
47
+ default: {
48
+ paddingLeft: theme.spacings['4'],
49
+ paddingRight: theme.spacings['6'],
50
+ minHeight: 56,
51
+ },
52
+ menuItem: {
53
+ paddingHorizontal: theme.spacings['3'],
54
+ minHeight: 40,
55
+ },
56
+ },
57
+ },
58
+ },
59
+ stateLayer: {
60
+ variants: {
61
+ state: {
62
+ hovered: {
63
+ backgroundColor: theme.colors.stateLayer.hover.onSurface,
64
+ },
65
+ focused: {
66
+ backgroundColor: theme.colors.stateLayer.hover.onSurface,
67
+ },
68
+ selectedAndFocused: {
69
+ backgroundColor: theme.colors.stateLayer.focussed.onSurface,
70
+ },
71
+ },
72
+ },
73
+ },
74
+ }));
75
+
76
+ export const listItemStyles = getRegisteredComponentStylesWithFallback(
77
+ 'List_Item',
78
+ listItemStylesDefault,
79
+ );
@@ -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);
@@ -0,0 +1,136 @@
1
+ import { memo, useId } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import {
5
+ cookie4Path,
6
+ cookie9Path,
7
+ expressFastSpatial,
8
+ loadingIndicatorStyles as componentStyles,
9
+ ovalPath,
10
+ pentagonPath,
11
+ pillPath,
12
+ type Props,
13
+ softBurstPath,
14
+ sunnyPath,
15
+ useProcessProps,
16
+ } from './utils';
17
+
18
+ /**
19
+ * Material 3 Expressive Loading Indicator for Web.
20
+ * Uses CSS 'd' attribute transitions for true shape morphing.
21
+ */
22
+ const LoadingIndicator = ({
23
+ animating = true,
24
+ color: colorProp,
25
+ size: sizeProp = 'md',
26
+ style,
27
+ variant = 'default',
28
+ innerContainerProps,
29
+ }: Props) => {
30
+ const id = useId();
31
+ componentStyles.useVariants({
32
+ variant: variant as 'contained',
33
+ });
34
+
35
+ const { size, strokeColor } = useProcessProps({
36
+ variant,
37
+ size: sizeProp,
38
+ color: colorProp,
39
+ });
40
+
41
+ if (!animating) return null;
42
+
43
+ return (
44
+ <View
45
+ style={[
46
+ componentStyles.container,
47
+ {
48
+ width: Math.floor((10 / 48) * size * 2) + size,
49
+ height: Math.floor((10 / 48) * size * 2) + size,
50
+ },
51
+ style,
52
+ ]}
53
+ accessible
54
+ accessibilityLabel="Loading"
55
+ accessibilityRole="progressbar"
56
+ accessibilityState={{ busy: animating }}>
57
+ <View
58
+ {...innerContainerProps}
59
+ style={[
60
+ { width: size, height: size },
61
+ componentStyles.innerContainer,
62
+ innerContainerProps?.style,
63
+ ]}>
64
+ <svg
65
+ className={`m3-expressive-svg-${id}`}
66
+ width={size}
67
+ height={size}
68
+ viewBox="0 0 38 38">
69
+ <path className={`m3-expressive-path-${id}`} fill={strokeColor} />
70
+ </svg>
71
+ </View>
72
+ <style>
73
+ {`
74
+ .m3-expressive-svg-${id} {
75
+ transform-origin: center;
76
+ display: block;
77
+ }
78
+
79
+ .m3-expressive-path-${id} {
80
+ animation: m3-expressive-combined 4.55s linear infinite;
81
+ transform-origin: center;
82
+ }
83
+
84
+ @keyframes m3-expressive-pulse-${id} {
85
+ 0%, 100% { transform: scale(1); }
86
+ 50% { transform: scale(1.1); }
87
+ }
88
+
89
+ @keyframes m3-expressive-combined {
90
+ 0% {
91
+ animation-timing-function: ${expressFastSpatial};
92
+ transform: rotate(0deg);
93
+ d: path('${softBurstPath}');
94
+ }
95
+ 14.28% {
96
+ animation-timing-function: ${expressFastSpatial};
97
+ transform: rotate(154.29deg);
98
+ d: path('${cookie9Path}');
99
+ }
100
+ 28.57% {
101
+ animation-timing-function: ${expressFastSpatial};
102
+ transform: rotate(308.57deg);
103
+ d: path('${pentagonPath}');
104
+ }
105
+ 42.85% {
106
+ animation-timing-function: ${expressFastSpatial};
107
+ transform: rotate(462.86deg);
108
+ d: path('${pillPath}');
109
+ }
110
+ 57.14% {
111
+ animation-timing-function: ${expressFastSpatial};
112
+ transform: rotate(617.14deg);
113
+ d: path('${sunnyPath}');
114
+ }
115
+ 71.42% {
116
+ animation-timing-function: ${expressFastSpatial};
117
+ transform: rotate(771.43deg);
118
+ d: path('${cookie4Path}');
119
+ }
120
+ 85.71% {
121
+ animation-timing-function: ${expressFastSpatial};
122
+ transform: rotate(925.71deg);
123
+ d: path('${ovalPath}');
124
+ }
125
+ 100% {
126
+ transform: rotate(1080deg);
127
+ d: path('${softBurstPath}');
128
+ }
129
+ }
130
+ `}
131
+ </style>
132
+ </View>
133
+ );
134
+ };
135
+
136
+ export default memo(LoadingIndicator);
@@ -0,0 +1,13 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ import LoadingIndicatorDefault from './LoadingIndicator';
3
+
4
+ export const LoadingIndicator = getRegisteredComponentWithFallback(
5
+ 'LoadingIndicator',
6
+ LoadingIndicatorDefault,
7
+ );
8
+
9
+ export {
10
+ type Props as LoadingIndicatorProps,
11
+ loadingIndicatorStyles,
12
+ loadingIndicatorStylesDefault,
13
+ } from './utils';