@webority-technologies/mobile 0.0.21 → 0.0.23

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 (187) hide show
  1. package/lib/commonjs/components/Accordion/Accordion.js +4 -2
  2. package/lib/commonjs/components/Avatar/Avatar.js +4 -2
  3. package/lib/commonjs/components/Badge/Badge.js +5 -5
  4. package/lib/commonjs/components/Banner/Banner.js +8 -4
  5. package/lib/commonjs/components/BottomNavigation/BottomNavigation.js +6 -4
  6. package/lib/commonjs/components/BottomSheet/BottomSheet.js +69 -13
  7. package/lib/commonjs/components/BottomSheet/index.js +6 -0
  8. package/lib/commonjs/components/Box/Box.js +162 -0
  9. package/lib/commonjs/components/Box/index.js +37 -0
  10. package/lib/commonjs/components/Button/Button.js +7 -7
  11. package/lib/commonjs/components/Carousel/Carousel.js +4 -2
  12. package/lib/commonjs/components/Checkbox/Checkbox.js +14 -5
  13. package/lib/commonjs/components/DatePicker/DatePicker.js +9 -7
  14. package/lib/commonjs/components/DateRangePicker/DateRangePicker.js +5 -2
  15. package/lib/commonjs/components/Dialog/Dialog.js +2 -2
  16. package/lib/commonjs/components/FieldBase/FieldBase.js +8 -4
  17. package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +13 -5
  18. package/lib/commonjs/components/FormField/FormField.js +61 -25
  19. package/lib/commonjs/components/Input/Input.js +41 -29
  20. package/lib/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +102 -0
  21. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +13 -0
  22. package/lib/commonjs/components/KeyboardToolbar/KeyboardToolbar.js +130 -0
  23. package/lib/commonjs/components/KeyboardToolbar/index.js +13 -0
  24. package/lib/commonjs/components/Modal/Modal.js +17 -6
  25. package/lib/commonjs/components/NumberInput/NumberInput.js +35 -28
  26. package/lib/commonjs/components/OTPInput/OTPInput.js +33 -18
  27. package/lib/commonjs/components/Radio/Radio.js +7 -5
  28. package/lib/commonjs/components/Radio/RadioGroup.js +10 -3
  29. package/lib/commonjs/components/SearchBar/SearchBar.js +4 -2
  30. package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +20 -10
  31. package/lib/commonjs/components/Select/Select.js +33 -32
  32. package/lib/commonjs/components/Skeleton/SkeletonContent.js +5 -2
  33. package/lib/commonjs/components/Slider/Slider.js +42 -26
  34. package/lib/commonjs/components/Spinner/Spinner.js +5 -5
  35. package/lib/commonjs/components/Switch/Switch.js +29 -16
  36. package/lib/commonjs/components/Tabs/Tabs.js +4 -2
  37. package/lib/commonjs/components/Text/Text.js +142 -0
  38. package/lib/commonjs/components/Text/index.js +13 -0
  39. package/lib/commonjs/components/TimePicker/TimePicker.js +10 -7
  40. package/lib/commonjs/components/Toast/Toast.js +22 -10
  41. package/lib/commonjs/components/Tooltip/Tooltip.js +6 -2
  42. package/lib/commonjs/components/index.js +141 -89
  43. package/lib/commonjs/form/FormContext.js +40 -0
  44. package/lib/commonjs/form/index.js +68 -0
  45. package/lib/commonjs/form/path.js +79 -0
  46. package/lib/commonjs/form/rules.js +67 -0
  47. package/lib/commonjs/form/types.js +2 -0
  48. package/lib/commonjs/form/useField.js +54 -0
  49. package/lib/commonjs/form/useForm.js +316 -0
  50. package/lib/commonjs/hooks/index.js +14 -0
  51. package/lib/commonjs/hooks/useControllableState.js +30 -0
  52. package/lib/commonjs/hooks/useReducedMotion.js +31 -0
  53. package/lib/commonjs/index.js +96 -11
  54. package/lib/commonjs/theme/ThemeContext.js +30 -2
  55. package/lib/commonjs/theme/tokens.js +12 -0
  56. package/lib/module/components/Accordion/Accordion.js +4 -2
  57. package/lib/module/components/Avatar/Avatar.js +4 -2
  58. package/lib/module/components/Badge/Badge.js +5 -5
  59. package/lib/module/components/Banner/Banner.js +8 -4
  60. package/lib/module/components/BottomNavigation/BottomNavigation.js +6 -4
  61. package/lib/module/components/BottomSheet/BottomSheet.js +68 -13
  62. package/lib/module/components/BottomSheet/index.js +1 -1
  63. package/lib/module/components/Box/Box.js +156 -0
  64. package/lib/module/components/Box/index.js +4 -0
  65. package/lib/module/components/Button/Button.js +7 -7
  66. package/lib/module/components/Carousel/Carousel.js +4 -2
  67. package/lib/module/components/Checkbox/Checkbox.js +14 -5
  68. package/lib/module/components/DatePicker/DatePicker.js +9 -7
  69. package/lib/module/components/DateRangePicker/DateRangePicker.js +5 -2
  70. package/lib/module/components/Dialog/Dialog.js +2 -2
  71. package/lib/module/components/FieldBase/FieldBase.js +8 -4
  72. package/lib/module/components/FloatingActionButton/FloatingActionButton.js +13 -5
  73. package/lib/module/components/FormField/FormField.js +62 -26
  74. package/lib/module/components/Input/Input.js +41 -29
  75. package/lib/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +98 -0
  76. package/lib/module/components/KeyboardAwareScrollView/index.js +4 -0
  77. package/lib/module/components/KeyboardToolbar/KeyboardToolbar.js +125 -0
  78. package/lib/module/components/KeyboardToolbar/index.js +4 -0
  79. package/lib/module/components/Modal/Modal.js +17 -6
  80. package/lib/module/components/NumberInput/NumberInput.js +30 -23
  81. package/lib/module/components/OTPInput/OTPInput.js +30 -15
  82. package/lib/module/components/Radio/Radio.js +7 -5
  83. package/lib/module/components/Radio/RadioGroup.js +10 -3
  84. package/lib/module/components/SearchBar/SearchBar.js +4 -2
  85. package/lib/module/components/SegmentedControl/SegmentedControl.js +20 -10
  86. package/lib/module/components/Select/Select.js +33 -32
  87. package/lib/module/components/Skeleton/SkeletonContent.js +5 -2
  88. package/lib/module/components/Slider/Slider.js +42 -26
  89. package/lib/module/components/Spinner/Spinner.js +5 -5
  90. package/lib/module/components/Switch/Switch.js +29 -16
  91. package/lib/module/components/Tabs/Tabs.js +4 -2
  92. package/lib/module/components/Text/Text.js +138 -0
  93. package/lib/module/components/Text/index.js +4 -0
  94. package/lib/module/components/TimePicker/TimePicker.js +10 -7
  95. package/lib/module/components/Toast/Toast.js +22 -10
  96. package/lib/module/components/Tooltip/Tooltip.js +6 -2
  97. package/lib/module/components/index.js +5 -1
  98. package/lib/module/form/FormContext.js +32 -0
  99. package/lib/module/form/index.js +12 -0
  100. package/lib/module/form/path.js +72 -0
  101. package/lib/module/form/rules.js +52 -0
  102. package/lib/module/form/types.js +2 -0
  103. package/lib/module/form/useField.js +49 -0
  104. package/lib/module/form/useForm.js +312 -0
  105. package/lib/module/hooks/index.js +2 -0
  106. package/lib/module/hooks/useControllableState.js +26 -0
  107. package/lib/module/hooks/useReducedMotion.js +27 -0
  108. package/lib/module/index.js +3 -1
  109. package/lib/module/theme/ThemeContext.js +30 -2
  110. package/lib/module/theme/tokens.js +12 -0
  111. package/lib/typescript/commonjs/components/BottomNavigation/BottomNavigation.d.ts +1 -1
  112. package/lib/typescript/commonjs/components/BottomSheet/BottomSheet.d.ts +41 -0
  113. package/lib/typescript/commonjs/components/BottomSheet/index.d.ts +2 -2
  114. package/lib/typescript/commonjs/components/Box/Box.d.ts +60 -0
  115. package/lib/typescript/commonjs/components/Box/index.d.ts +3 -0
  116. package/lib/typescript/commonjs/components/Button/Button.d.ts +1 -1
  117. package/lib/typescript/commonjs/components/Checkbox/Checkbox.d.ts +3 -2
  118. package/lib/typescript/commonjs/components/DatePicker/DatePicker.d.ts +3 -3
  119. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +2 -2
  120. package/lib/typescript/commonjs/components/FormField/FormField.d.ts +13 -2
  121. package/lib/typescript/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +20 -0
  122. package/lib/typescript/commonjs/components/KeyboardAwareScrollView/index.d.ts +3 -0
  123. package/lib/typescript/commonjs/components/KeyboardToolbar/KeyboardToolbar.d.ts +29 -0
  124. package/lib/typescript/commonjs/components/KeyboardToolbar/index.d.ts +3 -0
  125. package/lib/typescript/commonjs/components/NumberInput/NumberInput.d.ts +3 -2
  126. package/lib/typescript/commonjs/components/OTPInput/OTPInput.d.ts +3 -2
  127. package/lib/typescript/commonjs/components/Radio/Radio.d.ts +2 -2
  128. package/lib/typescript/commonjs/components/Radio/RadioGroup.d.ts +3 -2
  129. package/lib/typescript/commonjs/components/SegmentedControl/SegmentedControl.d.ts +3 -2
  130. package/lib/typescript/commonjs/components/Slider/Slider.d.ts +6 -4
  131. package/lib/typescript/commonjs/components/Spinner/Spinner.d.ts +1 -1
  132. package/lib/typescript/commonjs/components/Switch/Switch.d.ts +3 -2
  133. package/lib/typescript/commonjs/components/Text/Text.d.ts +25 -0
  134. package/lib/typescript/commonjs/components/Text/index.d.ts +3 -0
  135. package/lib/typescript/commonjs/components/TimePicker/TimePicker.d.ts +3 -3
  136. package/lib/typescript/commonjs/components/index.d.ts +10 -2
  137. package/lib/typescript/commonjs/form/FormContext.d.ts +17 -0
  138. package/lib/typescript/commonjs/form/index.d.ts +9 -0
  139. package/lib/typescript/commonjs/form/path.d.ts +10 -0
  140. package/lib/typescript/commonjs/form/rules.d.ts +31 -0
  141. package/lib/typescript/commonjs/form/types.d.ts +94 -0
  142. package/lib/typescript/commonjs/form/useField.d.ts +27 -0
  143. package/lib/typescript/commonjs/form/useForm.d.ts +10 -0
  144. package/lib/typescript/commonjs/hooks/index.d.ts +3 -0
  145. package/lib/typescript/commonjs/hooks/useControllableState.d.ts +17 -0
  146. package/lib/typescript/commonjs/hooks/useReducedMotion.d.ts +8 -0
  147. package/lib/typescript/commonjs/index.d.ts +4 -2
  148. package/lib/typescript/commonjs/theme/types.d.ts +15 -0
  149. package/lib/typescript/module/components/BottomNavigation/BottomNavigation.d.ts +1 -1
  150. package/lib/typescript/module/components/BottomSheet/BottomSheet.d.ts +41 -0
  151. package/lib/typescript/module/components/BottomSheet/index.d.ts +2 -2
  152. package/lib/typescript/module/components/Box/Box.d.ts +60 -0
  153. package/lib/typescript/module/components/Box/index.d.ts +3 -0
  154. package/lib/typescript/module/components/Button/Button.d.ts +1 -1
  155. package/lib/typescript/module/components/Checkbox/Checkbox.d.ts +3 -2
  156. package/lib/typescript/module/components/DatePicker/DatePicker.d.ts +3 -3
  157. package/lib/typescript/module/components/Dialog/Dialog.d.ts +2 -2
  158. package/lib/typescript/module/components/FormField/FormField.d.ts +13 -2
  159. package/lib/typescript/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +20 -0
  160. package/lib/typescript/module/components/KeyboardAwareScrollView/index.d.ts +3 -0
  161. package/lib/typescript/module/components/KeyboardToolbar/KeyboardToolbar.d.ts +29 -0
  162. package/lib/typescript/module/components/KeyboardToolbar/index.d.ts +3 -0
  163. package/lib/typescript/module/components/NumberInput/NumberInput.d.ts +3 -2
  164. package/lib/typescript/module/components/OTPInput/OTPInput.d.ts +3 -2
  165. package/lib/typescript/module/components/Radio/Radio.d.ts +2 -2
  166. package/lib/typescript/module/components/Radio/RadioGroup.d.ts +3 -2
  167. package/lib/typescript/module/components/SegmentedControl/SegmentedControl.d.ts +3 -2
  168. package/lib/typescript/module/components/Slider/Slider.d.ts +6 -4
  169. package/lib/typescript/module/components/Spinner/Spinner.d.ts +1 -1
  170. package/lib/typescript/module/components/Switch/Switch.d.ts +3 -2
  171. package/lib/typescript/module/components/Text/Text.d.ts +25 -0
  172. package/lib/typescript/module/components/Text/index.d.ts +3 -0
  173. package/lib/typescript/module/components/TimePicker/TimePicker.d.ts +3 -3
  174. package/lib/typescript/module/components/index.d.ts +10 -2
  175. package/lib/typescript/module/form/FormContext.d.ts +17 -0
  176. package/lib/typescript/module/form/index.d.ts +9 -0
  177. package/lib/typescript/module/form/path.d.ts +10 -0
  178. package/lib/typescript/module/form/rules.d.ts +31 -0
  179. package/lib/typescript/module/form/types.d.ts +94 -0
  180. package/lib/typescript/module/form/useField.d.ts +27 -0
  181. package/lib/typescript/module/form/useForm.d.ts +10 -0
  182. package/lib/typescript/module/hooks/index.d.ts +3 -0
  183. package/lib/typescript/module/hooks/useControllableState.d.ts +17 -0
  184. package/lib/typescript/module/hooks/useReducedMotion.d.ts +8 -0
  185. package/lib/typescript/module/index.d.ts +4 -2
  186. package/lib/typescript/module/theme/types.d.ts +15 -0
  187. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
4
  import { Animated, Easing, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
5
5
  import { fontFor, useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
+ import { useReducedMotion, useControllableState } from "../../hooks/index.js";
6
7
  import { triggerHaptic } from "../../utils/index.js";
7
8
  import { FieldBase } from "../FieldBase/FieldBase.js";
8
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -36,6 +37,7 @@ const sanitizeChar = (input, kind) => {
36
37
  const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
37
38
  const {
38
39
  value,
40
+ defaultValue,
39
41
  onChange,
40
42
  onComplete,
41
43
  length = 6,
@@ -51,6 +53,11 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
51
53
  textStyle,
52
54
  testID
53
55
  } = props;
56
+ const [current, setCurrent] = useControllableState({
57
+ value,
58
+ defaultValue: defaultValue ?? '',
59
+ onChange
60
+ });
54
61
  const theme = useTheme();
55
62
  const styles = useMemo(() => buildStyles(theme), [theme]);
56
63
  const sizeStyles = {
@@ -81,6 +88,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
81
88
  }, [length]);
82
89
  const previousFocusedIndexRef = useRef(-1);
83
90
  const shakeOnError = theme.components.otpInput?.shakeOnError ?? false;
91
+ const reduceMotion = useReducedMotion();
84
92
  const errorHaptic = theme.components.otpInput?.errorHaptic ?? false;
85
93
  const selectionHaptic = theme.components.otpInput?.selectionHaptic ?? false;
86
94
  const hasError = Boolean(error);
@@ -90,11 +98,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
90
98
  // error is initially true — there's no transition to animate.
91
99
  useEffect(() => {
92
100
  const isFirstRun = previousErrorRef.current === null;
101
+ let anim;
93
102
  if (!isFirstRun && hasError && !previousErrorRef.current) {
94
103
  if (errorHaptic) triggerHaptic('notificationError');
95
- if (shakeOnError) {
104
+ if (shakeOnError && !reduceMotion) {
96
105
  setNativeValue(shake, 0);
97
- Animated.sequence([Animated.timing(shake, {
106
+ anim = Animated.sequence([Animated.timing(shake, {
98
107
  toValue: 1,
99
108
  duration: 75,
100
109
  easing: Easing.linear,
@@ -114,10 +123,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
114
123
  duration: 75,
115
124
  easing: Easing.linear,
116
125
  useNativeDriver: true
117
- })]).start();
126
+ })]);
127
+ anim.start();
118
128
  }
119
129
  }
120
130
  previousErrorRef.current = hasError;
131
+ return () => anim?.stop();
121
132
  }, [hasError, shake, shakeOnError, errorHaptic]);
122
133
 
123
134
  // Animate underline opacity for the focused cell. Skip on first mount
@@ -127,17 +138,21 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
127
138
  previousFocusedIndexRef.current = focusedIndex;
128
139
  if (previous === focusedIndex) return;
129
140
  if (previous === -1 && focusedIndex === -1) return;
141
+ const anims = [];
130
142
  underlines.forEach((anim, idx) => {
131
143
  const target = idx === focusedIndex ? 1 : 0;
132
144
  // Only animate the cells whose target changed.
133
145
  if (idx !== focusedIndex && idx !== previous) return;
134
- Animated.timing(anim, {
146
+ const animation = Animated.timing(anim, {
135
147
  toValue: target,
136
148
  duration: theme.motion.duration.fast,
137
149
  easing: Easing.bezier(...theme.motion.easing.standard),
138
150
  useNativeDriver: true
139
- }).start();
151
+ });
152
+ animation.start();
153
+ anims.push(animation);
140
154
  });
155
+ return () => anims.forEach(a => a.stop());
141
156
  }, [focusedIndex, underlines, theme]);
142
157
  const focusCell = useCallback(index => {
143
158
  const target = inputsRef.current[index];
@@ -152,10 +167,10 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
152
167
  current?.blur();
153
168
  },
154
169
  clear: () => {
155
- onChange('');
170
+ setCurrent('');
156
171
  focusCell(0);
157
172
  }
158
- }), [focusCell, focusedIndex, onChange]);
173
+ }), [focusCell, focusedIndex, setCurrent]);
159
174
  useEffect(() => {
160
175
  if (autoFocus && !disabled) {
161
176
  // Defer one frame so the inputs are mounted.
@@ -166,19 +181,19 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
166
181
  }, [autoFocus, disabled, focusCell]);
167
182
  const cells = useMemo(() => {
168
183
  const arr = new Array(length).fill('');
169
- for (let i = 0; i < Math.min(value.length, length); i += 1) {
170
- arr[i] = value[i] ?? '';
184
+ for (let i = 0; i < Math.min(current.length, length); i += 1) {
185
+ arr[i] = current[i] ?? '';
171
186
  }
172
187
  return arr;
173
- }, [value, length]);
188
+ }, [current, length]);
174
189
  const updateValue = useCallback(next => {
175
190
  const trimmed = next.slice(0, length);
176
- if (trimmed === value) return;
177
- onChange(trimmed);
191
+ if (trimmed === current) return;
192
+ setCurrent(trimmed);
178
193
  if (trimmed.length === length && onComplete) {
179
194
  onComplete(trimmed);
180
195
  }
181
- }, [length, onChange, onComplete, value]);
196
+ }, [length, setCurrent, onComplete, current]);
182
197
  const handleChangeText = useCallback((index, raw) => {
183
198
  // Strip the ZWSP placeholder (used so iOS fires onKeyPress/Backspace on otherwise-empty cells).
184
199
  const stripped = raw.replace(/\u200B/g, '');
@@ -208,7 +223,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
208
223
  writeIndex += 1;
209
224
  }
210
225
  const next = chars.join('').slice(0, length);
211
- const previousLength = value.length;
226
+ const previousLength = current.length;
212
227
  updateValue(next);
213
228
  if (next.length !== previousLength && selectionHaptic) {
214
229
  triggerHaptic('selection');
@@ -221,7 +236,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
221
236
  } else {
222
237
  focusCell(nextFocus);
223
238
  }
224
- }, [cells, focusCell, keyboardType, length, selectionHaptic, updateValue, value]);
239
+ }, [cells, focusCell, keyboardType, length, selectionHaptic, updateValue, current]);
225
240
  const handleKeyPress = useCallback((index, e) => {
226
241
  const key = e.nativeEvent.key;
227
242
  if (key !== 'Backspace') return;
@@ -38,7 +38,7 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
38
38
  const {
39
39
  value,
40
40
  selected: selectedProp,
41
- onSelect,
41
+ onChange,
42
42
  disabled: disabledProp = false,
43
43
  label,
44
44
  size = 'md',
@@ -68,13 +68,15 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
68
68
  const resolvedHaptic = haptic === undefined ? pressHapticEnabled ? 'selection' : false : haptic;
69
69
  const progress = useRef(createAnimatedValue(selected ? 1 : 0)).current;
70
70
  useEffect(() => {
71
- Animated.spring(progress, {
71
+ const anim = Animated.spring(progress, {
72
72
  toValue: selected ? 1 : 0,
73
73
  damping: theme.motion.spring.snappy.damping,
74
74
  stiffness: theme.motion.spring.snappy.stiffness,
75
75
  mass: theme.motion.spring.snappy.mass,
76
76
  useNativeDriver: true
77
- }).start();
77
+ });
78
+ anim.start();
79
+ return () => anim.stop();
78
80
  }, [selected, progress, theme.motion.spring.snappy]);
79
81
  const scale = progress.interpolate({
80
82
  inputRange: [0, 1],
@@ -84,9 +86,9 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
84
86
  if (disabled) return;
85
87
  if (resolvedHaptic !== false) triggerHaptic(resolvedHaptic);
86
88
  if (ctx) {
87
- ctx.onSelect(value);
89
+ ctx.onChange(value);
88
90
  } else {
89
- onSelect?.(value);
91
+ onChange?.(value);
90
92
  }
91
93
  rest.onPressOut?.(event);
92
94
  };
@@ -3,9 +3,11 @@
3
3
  import React, { useMemo } from 'react';
4
4
  import { View } from 'react-native';
5
5
  import { RadioGroupContext } from "./Radio.js";
6
+ import { useControllableState } from "../../hooks/index.js";
6
7
  import { jsx as _jsx } from "react/jsx-runtime";
7
8
  const RadioGroup = ({
8
9
  value,
10
+ defaultValue,
9
11
  onChange,
10
12
  children,
11
13
  disabled = false,
@@ -13,11 +15,16 @@ const RadioGroup = ({
13
15
  style,
14
16
  testID
15
17
  }) => {
18
+ const [current, setCurrent] = useControllableState({
19
+ value,
20
+ defaultValue: defaultValue ?? undefined,
21
+ onChange: onChange
22
+ });
16
23
  const ctx = useMemo(() => ({
17
- selectedValue: value,
18
- onSelect: onChange,
24
+ selectedValue: current,
25
+ onChange: setCurrent,
19
26
  disabled
20
- }), [value, onChange, disabled]);
27
+ }), [current, setCurrent, disabled]);
21
28
  return /*#__PURE__*/_jsx(RadioGroupContext.Provider, {
22
29
  value: ctx,
23
30
  children: /*#__PURE__*/_jsx(View, {
@@ -75,12 +75,14 @@ const SearchBar = /*#__PURE__*/forwardRef((props, ref) => {
75
75
  }, [debouncedValue, debounceMs, onChangeText]);
76
76
  const showCancelButton = showCancel && isFocused;
77
77
  useEffect(() => {
78
- Animated.timing(cancelAnim, {
78
+ const anim = Animated.timing(cancelAnim, {
79
79
  toValue: showCancelButton ? 1 : 0,
80
80
  duration: 200,
81
81
  easing: Easing.bezier(...theme.motion.easing.standard),
82
82
  useNativeDriver: false
83
- }).start();
83
+ });
84
+ anim.start();
85
+ return () => anim.stop();
84
86
  }, [showCancelButton, cancelAnim, theme.motion.easing.standard]);
85
87
  const handleChangeText = useCallback(text => {
86
88
  if (debounceMs !== undefined) {
@@ -12,6 +12,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState }
12
12
  import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
13
13
  import { useTheme, createAnimatedValue } from "../../theme/index.js";
14
14
  import { triggerHaptic } from "../../utils/index.js";
15
+ import { useControllableState } from "../../hooks/index.js";
15
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
17
  const sizeMap = {
17
18
  sm: {
@@ -29,7 +30,8 @@ const TRACK_PADDING = 2;
29
30
  const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
30
31
  const {
31
32
  segments,
32
- value,
33
+ value: valueProp,
34
+ defaultValue,
33
35
  onChange,
34
36
  size = 'md',
35
37
  fullWidth = true,
@@ -44,6 +46,11 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
44
46
  labelStyle,
45
47
  testID
46
48
  } = props;
49
+ const [current, setCurrent] = useControllableState({
50
+ value: valueProp,
51
+ defaultValue: defaultValue ?? segments[0]?.value,
52
+ onChange
53
+ });
47
54
  const theme = useTheme();
48
55
  const segTheme = theme.components.segmentedControl;
49
56
  const styles = useMemo(() => buildStyles(theme), [theme]);
@@ -64,23 +71,26 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
64
71
  // only animate translateX via the native driver.
65
72
  const [trackWidth, setTrackWidth] = useState(0);
66
73
  const thumbTranslateX = useRef(createAnimatedValue(0)).current;
67
- const activeIndex = Math.max(0, segments.findIndex(s => s.value === value));
74
+ const activeIndex = Math.max(0, segments.findIndex(s => s.value === current));
68
75
  const segmentWidth = trackWidth > 0 ? (trackWidth - trackPadding * 2) / Math.max(segments.length, 1) : 0;
69
76
  const animateThumb = useCallback((index, segWidth) => {
70
- if (segWidth <= 0) return;
77
+ if (segWidth <= 0) return undefined;
71
78
  const targetX = trackPadding + segWidth * index;
72
79
  const spring = theme.motion.spring.snappy;
73
- Animated.spring(thumbTranslateX, {
80
+ const anim = Animated.spring(thumbTranslateX, {
74
81
  toValue: targetX,
75
82
  damping: spring.damping,
76
83
  stiffness: spring.stiffness,
77
84
  mass: spring.mass,
78
85
  useNativeDriver: true
79
- }).start();
86
+ });
87
+ anim.start();
88
+ return anim;
80
89
  }, [theme.motion.spring.snappy, thumbTranslateX]);
81
90
  useEffect(() => {
82
91
  if (segmentWidth > 0) {
83
- animateThumb(activeIndex, segmentWidth);
92
+ const anim = animateThumb(activeIndex, segmentWidth);
93
+ return () => anim?.stop();
84
94
  }
85
95
  }, [activeIndex, segmentWidth, animateThumb]);
86
96
  const handleTrackLayout = useCallback(e => {
@@ -91,10 +101,10 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
91
101
  }, [trackWidth]);
92
102
  const handlePress = useCallback(segment => {
93
103
  if (disabled) return;
94
- if (segment.value === value) return;
104
+ if (segment.value === current) return;
95
105
  if (changeHapticEnabled) triggerHaptic('selection');
96
- onChange(segment.value);
97
- }, [disabled, onChange, value, changeHapticEnabled]);
106
+ setCurrent(segment.value);
107
+ }, [disabled, setCurrent, current, changeHapticEnabled]);
98
108
  const thumbBg = tone === 'primary' ? theme.colors.background.elevated : theme.colors.background.elevated;
99
109
  const activeTextColor = tone === 'primary' ? theme.colors.text.primary : theme.colors.text.primary;
100
110
  return /*#__PURE__*/_jsxs(View, {
@@ -125,7 +135,7 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
125
135
  ...theme.shadows.sm
126
136
  }, selectedIndicatorStyle]
127
137
  }) : null, segments.map(segment => {
128
- const isActive = segment.value === value;
138
+ const isActive = segment.value === current;
129
139
  return /*#__PURE__*/_jsxs(Pressable, {
130
140
  onPress: () => handlePress(segment),
131
141
  disabled: disabled,
@@ -61,34 +61,32 @@ const Select = /*#__PURE__*/forwardRef((props, ref) => {
61
61
  const backdropAnim = useRef(createAnimatedValue(0)).current;
62
62
  const sheetAnim = useRef(createAnimatedValue(sheetMaxHeight)).current;
63
63
  useEffect(() => {
64
- if (open) {
65
- Animated.parallel([
66
- // Backdrop opacity uses JS driver — see Modal.tsx for the Fabric reason.
67
- Animated.timing(backdropAnim, {
68
- toValue: 1,
69
- duration: theme.motion.duration.fast,
70
- easing: Easing.bezier(...theme.motion.easing.standard),
71
- useNativeDriver: false
72
- }), Animated.spring(sheetAnim, {
73
- toValue: 0,
74
- damping: theme.motion.spring.snappy.damping,
75
- stiffness: theme.motion.spring.snappy.stiffness,
76
- mass: theme.motion.spring.snappy.mass,
77
- useNativeDriver: true
78
- })]).start();
79
- } else {
80
- Animated.parallel([Animated.timing(backdropAnim, {
81
- toValue: 0,
82
- duration: theme.motion.duration.fast,
83
- easing: Easing.bezier(...theme.motion.easing.standard),
84
- useNativeDriver: false
85
- }), Animated.timing(sheetAnim, {
86
- toValue: sheetMaxHeight,
87
- duration: theme.motion.duration.fast,
88
- easing: Easing.bezier(...theme.motion.easing.accelerate),
89
- useNativeDriver: true
90
- })]).start();
91
- }
64
+ const anim = open ? Animated.parallel([
65
+ // Backdrop opacity uses JS driver — see Modal.tsx for the Fabric reason.
66
+ Animated.timing(backdropAnim, {
67
+ toValue: 1,
68
+ duration: theme.motion.duration.fast,
69
+ easing: Easing.bezier(...theme.motion.easing.standard),
70
+ useNativeDriver: false
71
+ }), Animated.spring(sheetAnim, {
72
+ toValue: 0,
73
+ damping: theme.motion.spring.snappy.damping,
74
+ stiffness: theme.motion.spring.snappy.stiffness,
75
+ mass: theme.motion.spring.snappy.mass,
76
+ useNativeDriver: true
77
+ })]) : Animated.parallel([Animated.timing(backdropAnim, {
78
+ toValue: 0,
79
+ duration: theme.motion.duration.fast,
80
+ easing: Easing.bezier(...theme.motion.easing.standard),
81
+ useNativeDriver: false
82
+ }), Animated.timing(sheetAnim, {
83
+ toValue: sheetMaxHeight,
84
+ duration: theme.motion.duration.fast,
85
+ easing: Easing.bezier(...theme.motion.easing.accelerate),
86
+ useNativeDriver: true
87
+ })]);
88
+ anim.start();
89
+ return () => anim.stop();
92
90
  }, [open, backdropAnim, sheetAnim, sheetMaxHeight, theme.motion.duration.fast, theme.motion.easing.standard, theme.motion.easing.accelerate, theme.motion.spring.snappy]);
93
91
 
94
92
  // Selection helpers — typed to keep callbacks fully type-safe.
@@ -386,14 +384,17 @@ const Chevron = ({
386
384
  size,
387
385
  open
388
386
  }) => {
387
+ const theme = useTheme();
389
388
  const rotate = useRef(createAnimatedValue(open ? 1 : 0)).current;
390
389
  useEffect(() => {
391
- Animated.timing(rotate, {
390
+ const anim = Animated.timing(rotate, {
392
391
  toValue: open ? 1 : 0,
393
- duration: 160,
392
+ duration: theme.motion.duration.fast,
394
393
  useNativeDriver: true
395
- }).start();
396
- }, [open, rotate]);
394
+ });
395
+ anim.start();
396
+ return () => anim.stop();
397
+ }, [open, rotate, theme.motion.duration.fast]);
397
398
  const rotation = rotate.interpolate({
398
399
  inputRange: [0, 1],
399
400
  outputRange: ['0deg', '180deg']
@@ -216,19 +216,22 @@ const SkeletonContent = ({
216
216
  const wasLoadingRef = useRef(loading);
217
217
  const fadeAnim = useRef(createAnimatedValue(loading ? 0 : 1)).current;
218
218
  useEffect(() => {
219
+ let anim;
219
220
  if (wasLoadingRef.current && !loading) {
220
221
  fadeAnim.setValue(0);
221
- Animated.timing(fadeAnim, {
222
+ anim = Animated.timing(fadeAnim, {
222
223
  toValue: 1,
223
224
  duration: resolvedFadeDuration,
224
225
  useNativeDriver: true
225
- }).start();
226
+ });
227
+ anim.start();
226
228
  } else if (!wasLoadingRef.current && loading) {
227
229
  // Going back into loading from loaded — snap opacity so the
228
230
  // placeholder appears instantly when data invalidates.
229
231
  fadeAnim.setValue(1);
230
232
  }
231
233
  wasLoadingRef.current = loading;
234
+ return () => anim?.stop();
232
235
  }, [loading, fadeAnim, resolvedFadeDuration]);
233
236
  if (!loading) {
234
237
  if (resolvedFadeDuration <= 0) {
@@ -4,6 +4,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState }
4
4
  import { Animated, Easing, PanResponder, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
5
5
  import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { triggerHaptic } from "../../utils/index.js";
7
+ import { useControllableState } from "../../hooks/index.js";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  const SIZE_MAP = {
9
10
  sm: {
@@ -74,10 +75,15 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
74
75
  const [trackWidth, setTrackWidth] = useState(0);
75
76
  const fillColor = theme.colors[TONE_TO_COLOR_KEY[tone]];
76
77
  const isRange = props.range === true;
78
+ const [currentValue, setCurrentValue] = useControllableState({
79
+ value: props.value,
80
+ defaultValue: props.defaultValue ?? (isRange ? [min, max] : min),
81
+ onChange: props.onChange
82
+ });
77
83
 
78
84
  // Current value is mirrored in refs so PanResponder closures get fresh values.
79
- const lowRef = useRef(isRange ? props.value[0] : props.value);
80
- const highRef = useRef(isRange ? props.value[1] : props.value);
85
+ const lowRef = useRef(isRange ? currentValue[0] : currentValue);
86
+ const highRef = useRef(isRange ? currentValue[1] : currentValue);
81
87
  const lastReportedLow = useRef(lowRef.current);
82
88
  const lastReportedHigh = useRef(highRef.current);
83
89
 
@@ -115,7 +121,7 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
115
121
  useEffect(() => {
116
122
  if (trackWidth <= 0) return;
117
123
  if (isRange) {
118
- const [v0, v1] = props.value;
124
+ const [v0, v1] = currentValue;
119
125
  const lo = Math.min(v0, v1);
120
126
  const hi = Math.max(v0, v1);
121
127
  lowRef.current = lo;
@@ -125,38 +131,34 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
125
131
  setNativeValue(lowX, valueToPx(lo, trackWidth));
126
132
  setNativeValue(highX, valueToPx(hi, trackWidth));
127
133
  } else {
128
- const v = props.value;
134
+ const v = currentValue;
129
135
  lowRef.current = v;
130
136
  highRef.current = v;
131
137
  lastReportedLow.current = v;
132
138
  setNativeValue(lowX, valueToPx(v, trackWidth));
133
139
  }
134
- // We intentionally listen to props.value across both shapes via JSON; ESLint is fine.
140
+ // We intentionally listen to the value across both shapes via JSON; ESLint is fine.
135
141
  // eslint-disable-next-line react-hooks/exhaustive-deps
136
- }, [JSON.stringify(props.value), trackWidth, isRange, valueToPx]);
142
+ }, [JSON.stringify(currentValue), trackWidth, isRange, valueToPx]);
137
143
  const onTrackLayout = useCallback(e => {
138
144
  const w = e.nativeEvent.layout.width;
139
145
  setTrackWidth(w);
140
146
  // Initialize positions immediately.
141
147
  if (isRange) {
142
- const [v0, v1] = props.value;
148
+ const [v0, v1] = currentValue;
143
149
  const lo = Math.min(v0, v1);
144
150
  const hi = Math.max(v0, v1);
145
151
  setNativeValue(lowX, valueToPx(lo, w));
146
152
  setNativeValue(highX, valueToPx(hi, w));
147
153
  } else {
148
- setNativeValue(lowX, valueToPx(props.value, w));
154
+ setNativeValue(lowX, valueToPx(currentValue, w));
149
155
  }
150
156
  },
151
157
  // eslint-disable-next-line react-hooks/exhaustive-deps
152
158
  [isRange, valueToPx]);
153
159
  const fireChange = useCallback((lo, hi) => {
154
- if (isRange) {
155
- props.onChange([lo, hi]);
156
- } else {
157
- props.onChange(lo);
158
- }
159
- }, [isRange, props]);
160
+ setCurrentValue(isRange ? [lo, hi] : lo);
161
+ }, [isRange, setCurrentValue]);
160
162
  const animateThumbPress = useCallback((target, opacity, pressed) => {
161
163
  if (thumbPressAnimationEnabled) {
162
164
  Animated.spring(target, {
@@ -191,24 +193,29 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
191
193
  if (trackWidth <= 0) return;
192
194
  const nextPx = clamp(dragOffsetRef.current + gesture.dx, 0, trackWidth);
193
195
  let nextValue = pxToValue(nextPx, trackWidth);
196
+
197
+ // Only touch the native driver + fire onChange when the snapped value
198
+ // actually changes — between step boundaries the drag is a visual
199
+ // no-op (px is derived from the snapped value), so this drops the
200
+ // per-frame native round-trip and per-frame consumer re-render.
194
201
  if (which === 'low') {
195
202
  if (isRange && nextValue > highRef.current) nextValue = highRef.current;
196
203
  if (nextValue !== lastReportedLow.current) {
197
204
  if (sliderTheme?.stepHaptic ?? false) triggerHaptic('selection');
198
205
  lastReportedLow.current = nextValue;
206
+ lowRef.current = nextValue;
207
+ setNativeValue(lowX, valueToPx(nextValue, trackWidth));
208
+ fireChange(nextValue, highRef.current);
199
209
  }
200
- lowRef.current = nextValue;
201
- setNativeValue(lowX, valueToPx(nextValue, trackWidth));
202
- fireChange(nextValue, highRef.current);
203
210
  } else {
204
211
  if (isRange && nextValue < lowRef.current) nextValue = lowRef.current;
205
212
  if (nextValue !== lastReportedHigh.current) {
206
213
  if (sliderTheme?.stepHaptic ?? false) triggerHaptic('selection');
207
214
  lastReportedHigh.current = nextValue;
215
+ highRef.current = nextValue;
216
+ setNativeValue(highX, valueToPx(nextValue, trackWidth));
217
+ fireChange(lowRef.current, nextValue);
208
218
  }
209
- highRef.current = nextValue;
210
- setNativeValue(highX, valueToPx(nextValue, trackWidth));
211
- fireChange(lowRef.current, nextValue);
212
219
  }
213
220
  },
214
221
  onPanResponderRelease: () => {
@@ -267,12 +274,21 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
267
274
  }
268
275
  }
269
276
  }, [disabled, step, isRange, min, max, valueToPx, trackWidth, lowX, fireChange, sliderTheme]);
270
-
271
- // Computed values for fill bar.
272
- const fillLeft = isRange ? lowX : new Animated.Value(0);
273
- const fillWidth = isRange ? Animated.subtract(highX, lowX) : lowX;
274
277
  const thumbRadius = sizeCfg.thumbDiameter / 2;
275
278
 
279
+ // Stable Animated nodes for the fill bar and thumb offsets — created once and
280
+ // memoised so a render doesn't rebuild `new Animated.Value(...)` /
281
+ // Animated.subtract nodes and re-attach the animation graph every pass.
282
+ const zeroX = useRef(createAnimatedValue(0)).current;
283
+ const thumbRadiusX = useRef(createAnimatedValue(thumbRadius)).current;
284
+ useEffect(() => {
285
+ setNativeValue(thumbRadiusX, thumbRadius);
286
+ }, [thumbRadius, thumbRadiusX]);
287
+ const fillLeft = isRange ? lowX : zeroX;
288
+ const fillWidth = useMemo(() => isRange ? Animated.subtract(highX, lowX) : lowX, [isRange, highX, lowX]);
289
+ const lowTranslateX = useMemo(() => Animated.subtract(lowX, thumbRadiusX), [lowX, thumbRadiusX]);
290
+ const highTranslateX = useMemo(() => Animated.subtract(highX, thumbRadiusX), [highX, thumbRadiusX]);
291
+
276
292
  // Display values for labels.
277
293
  const lowDisplayValue = isRange ? lowRef.current : lowRef.current;
278
294
  const highDisplayValue = highRef.current;
@@ -325,7 +341,7 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
325
341
  backgroundColor: theme.colors.background.elevated,
326
342
  borderColor: disabled ? theme.colors.surface.disabled : fillColor,
327
343
  transform: [{
328
- translateX: Animated.subtract(lowX, new Animated.Value(thumbRadius))
344
+ translateX: lowTranslateX
329
345
  }, {
330
346
  scale: lowScale
331
347
  }],
@@ -353,7 +369,7 @@ const Slider = /*#__PURE__*/forwardRef((props, ref) => {
353
369
  backgroundColor: theme.colors.background.elevated,
354
370
  borderColor: disabled ? theme.colors.surface.disabled : fillColor,
355
371
  transform: [{
356
- translateX: Animated.subtract(highX, new Animated.Value(thumbRadius))
372
+ translateX: highTranslateX
357
373
  }, {
358
374
  scale: highScale
359
375
  }],
@@ -5,9 +5,9 @@ import { ActivityIndicator, Animated, Easing, StyleSheet, Text, View } from 'rea
5
5
  import { useTheme, createAnimatedValue } from "../../theme/index.js";
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  const sizeMap = {
8
- small: 20,
9
- medium: 32,
10
- large: 48
8
+ sm: 20,
9
+ md: 32,
10
+ lg: 48
11
11
  };
12
12
  const DOT_COUNT = 3;
13
13
  const DOT_STAGGER_MS = 150;
@@ -50,7 +50,7 @@ const Dot = ({
50
50
  };
51
51
  const Spinner = /*#__PURE__*/forwardRef((props, ref) => {
52
52
  const {
53
- size = 'medium',
53
+ size = 'md',
54
54
  color,
55
55
  message,
56
56
  overlay = false,
@@ -93,7 +93,7 @@ const Spinner = /*#__PURE__*/forwardRef((props, ref) => {
93
93
  }
94
94
  return /*#__PURE__*/_jsx(ActivityIndicator, {
95
95
  color: resolvedColor,
96
- size: size === 'small' ? 'small' : 'large',
96
+ size: size === 'sm' ? 'small' : 'large',
97
97
  style: {
98
98
  width: dimension,
99
99
  height: dimension