jfs-components 0.0.62 → 0.0.64

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 (255) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +1 -1
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +1 -1
  4. package/lib/commonjs/components/ActionTile/ActionTile.js +2 -1
  5. package/lib/commonjs/components/AmountInput/AmountInput.js +2 -1
  6. package/lib/commonjs/components/AppBar/AppBar.js +1 -1
  7. package/lib/commonjs/components/Avatar/Avatar.js +184 -162
  8. package/lib/commonjs/components/AvatarGroup/AvatarGroup.js +1 -1
  9. package/lib/commonjs/components/Badge/Badge.js +2 -1
  10. package/lib/commonjs/components/Balance/Balance.js +2 -1
  11. package/lib/commonjs/components/BottomNav/BottomNav.js +2 -1
  12. package/lib/commonjs/components/BottomNavItem/BottomNavItem.js +106 -86
  13. package/lib/commonjs/components/Button/Button.js +190 -93
  14. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  15. package/lib/commonjs/components/Card/Card.js +2 -1
  16. package/lib/commonjs/components/CardCTA/CardCTA.js +1 -1
  17. package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +1 -1
  18. package/lib/commonjs/components/Carousel/Carousel.js +3 -2
  19. package/lib/commonjs/components/Checkbox/Checkbox.js +2 -1
  20. package/lib/commonjs/components/ChipGroup/ChipGroup.js +1 -1
  21. package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
  22. package/lib/commonjs/components/DebitCard/DebitCard.js +1 -1
  23. package/lib/commonjs/components/Disclaimer/Disclaimer.js +2 -1
  24. package/lib/commonjs/components/Divider/Divider.js +2 -1
  25. package/lib/commonjs/components/Drawer/Drawer.js +109 -48
  26. package/lib/commonjs/components/EmptyState/EmptyState.js +2 -1
  27. package/lib/commonjs/components/FilterBar/FilterBar.js +1 -1
  28. package/lib/commonjs/components/Form/Form.js +2 -1
  29. package/lib/commonjs/components/FormField/FormField.js +3 -2
  30. package/lib/commonjs/components/HStack/HStack.js +1 -1
  31. package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -1
  32. package/lib/commonjs/components/IconButton/IconButton.js +118 -128
  33. package/lib/commonjs/components/IconCapsule/IconCapsule.js +61 -57
  34. package/lib/commonjs/components/InputSearch/InputSearch.js +7 -3
  35. package/lib/commonjs/components/LazyList/LazyList.js +1 -1
  36. package/lib/commonjs/components/LinearMeter/LinearMeter.js +3 -2
  37. package/lib/commonjs/components/ListGroup/ListGroup.js +1 -1
  38. package/lib/commonjs/components/ListItem/ListItem.js +190 -142
  39. package/lib/commonjs/components/MediaCard/MediaCard.js +3 -3
  40. package/lib/commonjs/components/MerchantProfile/MerchantProfile.js +2 -1
  41. package/lib/commonjs/components/MoneyValue/MoneyValue.js +2 -1
  42. package/lib/commonjs/components/NavArrow/NavArrow.js +82 -59
  43. package/lib/commonjs/components/NoteInput/NoteInput.js +2 -1
  44. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  45. package/lib/commonjs/components/Numpad/Numpad.js +2 -1
  46. package/lib/commonjs/components/OTP/OTP.js +1 -1
  47. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +2 -1
  48. package/lib/commonjs/components/Popup/Popup.js +2 -1
  49. package/lib/commonjs/components/ProductLabel/ProductLabel.js +2 -1
  50. package/lib/commonjs/components/ProgressBadge/ProgressBadge.js +2 -1
  51. package/lib/commonjs/components/RadioButton/RadioButton.js +2 -1
  52. package/lib/commonjs/components/RechargeCard/RechargeCard.js +2 -1
  53. package/lib/commonjs/components/Screen/Screen.js +1 -1
  54. package/lib/commonjs/components/Section/Section.js +500 -166
  55. package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +3 -2
  56. package/lib/commonjs/components/StatItem/StatItem.js +2 -1
  57. package/lib/commonjs/components/StatusHero/StatusHero.js +2 -1
  58. package/lib/commonjs/components/Stepper/Step.js +2 -1
  59. package/lib/commonjs/components/Stepper/StepLabel.js +2 -1
  60. package/lib/commonjs/components/Stepper/Stepper.js +2 -1
  61. package/lib/commonjs/components/SupportText/SupportText.js +2 -1
  62. package/lib/commonjs/components/SupportText/SupportTextIcon.js +2 -1
  63. package/lib/commonjs/components/SwappableAmount/SwappableAmount.js +2 -1
  64. package/lib/commonjs/components/Tabs/TabItem.js +2 -1
  65. package/lib/commonjs/components/Tabs/Tabs.js +2 -1
  66. package/lib/commonjs/components/Text/Text.js +2 -1
  67. package/lib/commonjs/components/TextInput/TextInput.js +2 -2
  68. package/lib/commonjs/components/ThreadHero/ThreadHero.js +2 -1
  69. package/lib/commonjs/components/Title/Title.js +2 -1
  70. package/lib/commonjs/components/Toast/Toast.js +2 -1
  71. package/lib/commonjs/components/Toggle/Toggle.js +2 -1
  72. package/lib/commonjs/components/Tooltip/Tooltip.js +2 -1
  73. package/lib/commonjs/components/TransactionBubble/TransactionBubble.js +1 -1
  74. package/lib/commonjs/components/TransactionDetails/TransactionDetails.js +2 -2
  75. package/lib/commonjs/components/TransactionStatus/TransactionStatus.js +3 -2
  76. package/lib/commonjs/components/UpiHandle/UpiHandle.js +144 -110
  77. package/lib/commonjs/components/VStack/VStack.js +1 -1
  78. package/lib/commonjs/design-tokens/figma-variables-resolver.js +21 -3
  79. package/lib/commonjs/icons/registry.js +1 -1
  80. package/lib/commonjs/utils/react-utils.js +17 -0
  81. package/lib/module/components/Accordion/Accordion.js +2 -2
  82. package/lib/module/components/ActionFooter/ActionFooter.js +2 -2
  83. package/lib/module/components/ActionTile/ActionTile.js +2 -1
  84. package/lib/module/components/AmountInput/AmountInput.js +2 -1
  85. package/lib/module/components/AppBar/AppBar.js +2 -2
  86. package/lib/module/components/Avatar/Avatar.js +184 -162
  87. package/lib/module/components/AvatarGroup/AvatarGroup.js +2 -2
  88. package/lib/module/components/Badge/Badge.js +2 -1
  89. package/lib/module/components/Balance/Balance.js +2 -1
  90. package/lib/module/components/BottomNav/BottomNav.js +2 -1
  91. package/lib/module/components/BottomNavItem/BottomNavItem.js +108 -88
  92. package/lib/module/components/Button/Button.js +192 -95
  93. package/lib/module/components/ButtonGroup/ButtonGroup.js +2 -2
  94. package/lib/module/components/Card/Card.js +2 -1
  95. package/lib/module/components/CardCTA/CardCTA.js +2 -2
  96. package/lib/module/components/CardProviderInfo/CardProviderInfo.js +2 -2
  97. package/lib/module/components/Carousel/Carousel.js +3 -2
  98. package/lib/module/components/Checkbox/Checkbox.js +2 -1
  99. package/lib/module/components/ChipGroup/ChipGroup.js +2 -2
  100. package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
  101. package/lib/module/components/DebitCard/DebitCard.js +2 -2
  102. package/lib/module/components/Disclaimer/Disclaimer.js +2 -1
  103. package/lib/module/components/Divider/Divider.js +2 -1
  104. package/lib/module/components/Drawer/Drawer.js +109 -48
  105. package/lib/module/components/EmptyState/EmptyState.js +2 -1
  106. package/lib/module/components/FilterBar/FilterBar.js +2 -2
  107. package/lib/module/components/Form/Form.js +2 -1
  108. package/lib/module/components/FormField/FormField.js +3 -2
  109. package/lib/module/components/HStack/HStack.js +2 -2
  110. package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -1
  111. package/lib/module/components/IconButton/IconButton.js +120 -130
  112. package/lib/module/components/IconCapsule/IconCapsule.js +60 -57
  113. package/lib/module/components/InputSearch/InputSearch.js +7 -3
  114. package/lib/module/components/LazyList/LazyList.js +2 -2
  115. package/lib/module/components/LinearMeter/LinearMeter.js +3 -2
  116. package/lib/module/components/ListGroup/ListGroup.js +2 -2
  117. package/lib/module/components/ListItem/ListItem.js +194 -146
  118. package/lib/module/components/MediaCard/MediaCard.js +4 -2
  119. package/lib/module/components/MerchantProfile/MerchantProfile.js +2 -1
  120. package/lib/module/components/MoneyValue/MoneyValue.js +2 -1
  121. package/lib/module/components/NavArrow/NavArrow.js +82 -58
  122. package/lib/module/components/NoteInput/NoteInput.js +2 -1
  123. package/lib/module/components/Nudge/Nudge.js +2 -2
  124. package/lib/module/components/Numpad/Numpad.js +2 -1
  125. package/lib/module/components/OTP/OTP.js +2 -2
  126. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +2 -1
  127. package/lib/module/components/Popup/Popup.js +2 -1
  128. package/lib/module/components/ProductLabel/ProductLabel.js +2 -1
  129. package/lib/module/components/ProgressBadge/ProgressBadge.js +2 -1
  130. package/lib/module/components/RadioButton/RadioButton.js +2 -1
  131. package/lib/module/components/RechargeCard/RechargeCard.js +2 -1
  132. package/lib/module/components/Screen/Screen.js +2 -2
  133. package/lib/module/components/Section/Section.js +503 -169
  134. package/lib/module/components/SegmentedControl/SegmentedControl.js +3 -2
  135. package/lib/module/components/StatItem/StatItem.js +2 -1
  136. package/lib/module/components/StatusHero/StatusHero.js +2 -1
  137. package/lib/module/components/Stepper/Step.js +2 -1
  138. package/lib/module/components/Stepper/StepLabel.js +2 -1
  139. package/lib/module/components/Stepper/Stepper.js +2 -1
  140. package/lib/module/components/SupportText/SupportText.js +2 -1
  141. package/lib/module/components/SupportText/SupportTextIcon.js +2 -1
  142. package/lib/module/components/SwappableAmount/SwappableAmount.js +2 -1
  143. package/lib/module/components/Tabs/TabItem.js +2 -1
  144. package/lib/module/components/Tabs/Tabs.js +2 -1
  145. package/lib/module/components/Text/Text.js +2 -1
  146. package/lib/module/components/TextInput/TextInput.js +3 -3
  147. package/lib/module/components/ThreadHero/ThreadHero.js +2 -1
  148. package/lib/module/components/Title/Title.js +2 -1
  149. package/lib/module/components/Toast/Toast.js +2 -1
  150. package/lib/module/components/Toggle/Toggle.js +2 -1
  151. package/lib/module/components/Tooltip/Tooltip.js +2 -1
  152. package/lib/module/components/TransactionBubble/TransactionBubble.js +2 -2
  153. package/lib/module/components/TransactionDetails/TransactionDetails.js +3 -3
  154. package/lib/module/components/TransactionStatus/TransactionStatus.js +3 -2
  155. package/lib/module/components/UpiHandle/UpiHandle.js +147 -113
  156. package/lib/module/components/VStack/VStack.js +2 -2
  157. package/lib/module/design-tokens/figma-variables-resolver.js +21 -3
  158. package/lib/module/icons/registry.js +1 -1
  159. package/lib/module/utils/react-utils.js +16 -0
  160. package/lib/typescript/src/components/Avatar/Avatar.d.ts +11 -17
  161. package/lib/typescript/src/components/BottomNavItem/BottomNavItem.d.ts +12 -8
  162. package/lib/typescript/src/components/Button/Button.d.ts +18 -1
  163. package/lib/typescript/src/components/IconButton/IconButton.d.ts +12 -29
  164. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +10 -18
  165. package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +8 -3
  166. package/lib/typescript/src/components/ListItem/ListItem.d.ts +14 -1
  167. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +12 -11
  168. package/lib/typescript/src/components/Section/Section.d.ts +43 -48
  169. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +13 -12
  170. package/lib/typescript/src/icons/registry.d.ts +1 -1
  171. package/lib/typescript/src/utils/react-utils.d.ts +15 -0
  172. package/package.json +4 -6
  173. package/src/components/Accordion/Accordion.tsx +2 -2
  174. package/src/components/ActionFooter/ActionFooter.tsx +2 -2
  175. package/src/components/ActionTile/ActionTile.tsx +2 -1
  176. package/src/components/AmountInput/AmountInput.tsx +2 -1
  177. package/src/components/AppBar/AppBar.tsx +2 -2
  178. package/src/components/Avatar/Avatar.tsx +229 -158
  179. package/src/components/AvatarGroup/AvatarGroup.tsx +2 -2
  180. package/src/components/Badge/Badge.tsx +2 -1
  181. package/src/components/Balance/Balance.tsx +2 -1
  182. package/src/components/BottomNav/BottomNav.tsx +2 -1
  183. package/src/components/BottomNavItem/BottomNavItem.tsx +159 -88
  184. package/src/components/Button/Button.tsx +228 -101
  185. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  186. package/src/components/Card/Card.tsx +2 -1
  187. package/src/components/CardCTA/CardCTA.tsx +2 -2
  188. package/src/components/CardProviderInfo/CardProviderInfo.tsx +2 -2
  189. package/src/components/Carousel/Carousel.tsx +3 -2
  190. package/src/components/Checkbox/Checkbox.tsx +2 -1
  191. package/src/components/ChipGroup/ChipGroup.tsx +2 -2
  192. package/src/components/ChipSelect/ChipSelect.tsx +2 -1
  193. package/src/components/DebitCard/DebitCard.tsx +2 -2
  194. package/src/components/Disclaimer/Disclaimer.tsx +2 -1
  195. package/src/components/Divider/Divider.tsx +2 -1
  196. package/src/components/Drawer/Drawer.tsx +124 -58
  197. package/src/components/EmptyState/EmptyState.tsx +2 -1
  198. package/src/components/FilterBar/FilterBar.tsx +2 -2
  199. package/src/components/Form/Form.tsx +2 -1
  200. package/src/components/FormField/FormField.tsx +3 -2
  201. package/src/components/HStack/HStack.tsx +2 -2
  202. package/src/components/HoldingsCard/HoldingsCard.tsx +2 -1
  203. package/src/components/IconButton/IconButton.tsx +154 -126
  204. package/src/components/IconCapsule/IconCapsule.tsx +73 -54
  205. package/src/components/InputSearch/InputSearch.tsx +19 -5
  206. package/src/components/LazyList/LazyList.tsx +2 -2
  207. package/src/components/LinearMeter/LinearMeter.tsx +3 -2
  208. package/src/components/ListGroup/ListGroup.tsx +2 -2
  209. package/src/components/ListItem/ListItem.tsx +257 -187
  210. package/src/components/MediaCard/MediaCard.tsx +2 -1
  211. package/src/components/MerchantProfile/MerchantProfile.tsx +2 -1
  212. package/src/components/MoneyValue/MoneyValue.tsx +2 -1
  213. package/src/components/NavArrow/NavArrow.tsx +91 -58
  214. package/src/components/NoteInput/NoteInput.tsx +2 -1
  215. package/src/components/Nudge/Nudge.tsx +2 -2
  216. package/src/components/Numpad/Numpad.tsx +2 -1
  217. package/src/components/OTP/OTP.tsx +2 -2
  218. package/src/components/PaymentFeedback/PaymentFeedback.tsx +2 -1
  219. package/src/components/Popup/Popup.tsx +2 -1
  220. package/src/components/ProductLabel/ProductLabel.tsx +2 -1
  221. package/src/components/ProgressBadge/ProgressBadge.tsx +2 -2
  222. package/src/components/RadioButton/RadioButton.tsx +2 -1
  223. package/src/components/RechargeCard/RechargeCard.tsx +2 -1
  224. package/src/components/Screen/Screen.tsx +2 -2
  225. package/src/components/Section/Section.tsx +672 -176
  226. package/src/components/SegmentedControl/SegmentedControl.tsx +3 -2
  227. package/src/components/StatItem/StatItem.tsx +2 -1
  228. package/src/components/StatusHero/StatusHero.tsx +2 -1
  229. package/src/components/Stepper/Step.tsx +2 -1
  230. package/src/components/Stepper/StepLabel.tsx +2 -1
  231. package/src/components/Stepper/Stepper.tsx +2 -1
  232. package/src/components/SupportText/SupportText.tsx +2 -1
  233. package/src/components/SupportText/SupportTextIcon.tsx +2 -1
  234. package/src/components/SwappableAmount/SwappableAmount.tsx +2 -1
  235. package/src/components/Tabs/TabItem.tsx +2 -1
  236. package/src/components/Tabs/Tabs.tsx +2 -1
  237. package/src/components/Text/Text.tsx +2 -1
  238. package/src/components/TextInput/TextInput.tsx +3 -3
  239. package/src/components/ThreadHero/ThreadHero.tsx +2 -1
  240. package/src/components/Title/Title.tsx +2 -1
  241. package/src/components/Toast/Toast.tsx +2 -1
  242. package/src/components/Toggle/Toggle.tsx +2 -1
  243. package/src/components/Tooltip/Tooltip.tsx +2 -1
  244. package/src/components/TransactionBubble/TransactionBubble.tsx +2 -2
  245. package/src/components/TransactionDetails/TransactionDetails.tsx +3 -3
  246. package/src/components/TransactionStatus/TransactionStatus.tsx +3 -2
  247. package/src/components/UpiHandle/UpiHandle.tsx +193 -125
  248. package/src/components/VStack/VStack.tsx +2 -2
  249. package/src/design-tokens/figma-variables-resolver.ts +21 -3
  250. package/src/icons/registry.ts +1 -1
  251. package/src/utils/react-utils.ts +16 -0
  252. package/lib/typescript/App.d.ts +0 -2
  253. package/lib/typescript/index.d.ts +0 -2
  254. package/lib/typescript/metro.config.d.ts +0 -78
  255. package/lib/typescript/react-native.config.d.ts +0 -4
@@ -1,18 +1,20 @@
1
- import React, { useMemo } from 'react'
2
- import { View, Text, Pressable, type StyleProp, type ViewStyle, type TextStyle, type AccessibilityState } from 'react-native'
1
+ import React, { useCallback, useMemo, useRef } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ Platform,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type TextStyle,
10
+ type AccessibilityState,
11
+ type PressableStateCallbackType,
12
+ } from 'react-native'
3
13
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
14
  import IconCapsule from '../IconCapsule/IconCapsule'
5
15
  import NavArrow from '../NavArrow/NavArrow'
6
16
  import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
7
-
8
- /**
9
- * Helper function to recursively clone children and pass modes prop to components that accept it.
10
- * This ensures that all child components in slots receive the modes prop from the parent.
11
- *
12
- * @param forcedModes - Optional modes that will ALWAYS be applied last, overriding any other modes.
13
- * Useful for slots that need to enforce specific context values.
14
- */
15
- import { cloneChildrenWithModes } from '../../utils/react-utils'
17
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
16
18
 
17
19
  type ListItemProps = {
18
20
  layout?: 'Vertical' | 'Horizontal';
@@ -37,6 +39,113 @@ type ListItemProps = {
37
39
  webAccessibilityProps?: WebAccessibilityProps;
38
40
  } & React.ComponentProps<typeof View>;
39
41
 
42
+ // ---------------------------------------------------------------------------
43
+ // Module-scope constants — never re-allocated per render.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const IS_IOS = Platform.OS === 'ios'
47
+ const PRESS_DELAY = IS_IOS ? 130 : 0
48
+
49
+ // Forced modes for the endSlot — `Context: 'ListItem'` can never be
50
+ // overridden by external modes. Frozen so identity is stable across renders.
51
+ const END_SLOT_FORCED_MODES = Object.freeze({ Context: 'ListItem' })
52
+
53
+ // Pressed visual is applied on the host view through Pressable's style
54
+ // callback, so a scroll-cancelled touch never schedules a React render.
55
+ const pressedOverlayStyle: ViewStyle = { opacity: 0.85 }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Token resolution — collapsed into one useMemo per (modes) tuple.
59
+ // ---------------------------------------------------------------------------
60
+
61
+ interface ListItemTokens {
62
+ baseContainerStyle: ViewStyle;
63
+ horizontalLayoutStyle: ViewStyle;
64
+ verticalLayoutStyle: ViewStyle;
65
+ innerContentBaseStyle: ViewStyle;
66
+ verticalContentBaseStyle: ViewStyle;
67
+ titleTextStyle: TextStyle;
68
+ supportTextStyle: TextStyle;
69
+ trailingWrapperStyle: ViewStyle;
70
+ textWrapGap: number;
71
+ resolvedModes: Record<string, any>;
72
+ }
73
+
74
+ function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
75
+ const resolvedModes = { ...modes, Context: 'ListItem' }
76
+
77
+ const gap = (getVariableByName('listItem/gap', resolvedModes) ?? 8) as number
78
+ const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0
79
+ const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0
80
+ const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0
81
+ const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0
82
+ const textWrapGap = (getVariableByName('listItem/text wrap', resolvedModes) ?? 0) as number
83
+
84
+ const titleColor = getVariableByName('listItem/title/color', resolvedModes) || '#0f0d0a'
85
+ const titleFontSize = getVariableByName('listItem/title/fontSize', resolvedModes) || 14
86
+ const titleLineHeight = getVariableByName('listItem/title/lineHeight', resolvedModes) || 16
87
+ const titleFontFamily = getVariableByName('listItem/title/fontFamily', resolvedModes) || 'System'
88
+ const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', resolvedModes) || 700
89
+ const titleFontWeight =
90
+ typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw
91
+
92
+ const supportColor = getVariableByName('listItem/supportText/color', resolvedModes) || '#1f1a14'
93
+ const supportFontSize = getVariableByName('listItem/supportText/fontSize', resolvedModes) || 12
94
+ const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', resolvedModes) || 14
95
+ const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', resolvedModes) || 'System'
96
+ const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', resolvedModes) || 500
97
+ const supportFontWeight =
98
+ typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw
99
+
100
+ return {
101
+ baseContainerStyle: {
102
+ paddingTop: paddingTop as number,
103
+ paddingBottom: paddingBottom as number,
104
+ paddingLeft: paddingLeft as number,
105
+ paddingRight: paddingRight as number,
106
+ },
107
+ horizontalLayoutStyle: {
108
+ flexDirection: 'row',
109
+ alignItems: 'center',
110
+ gap,
111
+ },
112
+ verticalLayoutStyle: {
113
+ flexDirection: 'column',
114
+ alignItems: 'center',
115
+ gap,
116
+ },
117
+ innerContentBaseStyle: {
118
+ flexDirection: 'row',
119
+ alignItems: 'center',
120
+ flex: 1,
121
+ gap,
122
+ },
123
+ verticalContentBaseStyle: {
124
+ alignItems: 'center',
125
+ gap,
126
+ },
127
+ titleTextStyle: {
128
+ color: titleColor as string,
129
+ fontSize: titleFontSize as number,
130
+ lineHeight: titleLineHeight as number,
131
+ fontFamily: titleFontFamily as string,
132
+ fontWeight: titleFontWeight as TextStyle['fontWeight'],
133
+ },
134
+ supportTextStyle: {
135
+ color: supportColor as string,
136
+ fontSize: supportFontSize as number,
137
+ lineHeight: supportLineHeight as number,
138
+ fontFamily: supportFontFamily as string,
139
+ fontWeight: supportFontWeight as TextStyle['fontWeight'],
140
+ },
141
+ trailingWrapperStyle: { marginLeft: gap / 2 },
142
+ textWrapGap,
143
+ resolvedModes,
144
+ }
145
+ }
146
+
147
+ const verticalSupportTextOverride: TextStyle = { textAlign: 'center' }
148
+
40
149
  /**
41
150
  * ListItem component that mirrors the Figma "List Item" component.
42
151
  *
@@ -68,8 +177,20 @@ type ListItemProps = {
68
177
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers. If not provided, uses title and supportText
69
178
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
70
179
  * @param {Object} [props.accessibilityState] - Additional accessibility state information
180
+ *
181
+ * Performance notes:
182
+ * - All token reads are folded into a single `useMemo([modes])`.
183
+ * - The pressed visual is applied via Pressable's `style={({pressed}) => ...}`
184
+ * callback so RN updates the host view directly — no React render is
185
+ * scheduled by a touch. On iOS the touch is additionally delayed via
186
+ * `unstable_pressDelay={130}` so a scroll-cancelled tap never even
187
+ * transiently applies the pressed style. This is the fix for the
188
+ * "People / Setup card flicker during scroll" report.
189
+ * - User-supplied event handlers are read through a ref so the wrapper
190
+ * handlers stay referentially stable across renders.
191
+ * - The component is wrapped in `React.memo`.
71
192
  */
72
- function ListItem({
193
+ function ListItemImpl({
73
194
  layout = 'Vertical',
74
195
  title = 'Title',
75
196
  supportText = 'Support Text',
@@ -78,7 +199,7 @@ function ListItem({
78
199
  supportSlot,
79
200
  endSlot,
80
201
  navArrow = true,
81
- modes = {},
202
+ modes = EMPTY_MODES,
82
203
  onPress,
83
204
  style,
84
205
  contentStyle,
@@ -88,98 +209,17 @@ function ListItem({
88
209
  webAccessibilityProps,
89
210
  ...rest
90
211
  }: ListItemProps) {
91
- const resolvedModes = { ...modes, "Context": "ListItem" }
92
-
93
- // Resolve container spacing tokens
94
- const gap = getVariableByName('listItem/gap', resolvedModes) ?? 8
95
- const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0
96
- const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0
97
- const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0
98
- const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0
99
- const textWrapGap = getVariableByName('listItem/text wrap', resolvedModes) ?? 0
100
-
101
- // Title typography (horizontal layout)
102
- const titleColor = getVariableByName('listItem/title/color', resolvedModes) || '#0f0d0a'
103
- const titleFontSize = getVariableByName('listItem/title/fontSize', resolvedModes) || 14
104
- const titleLineHeight = getVariableByName('listItem/title/lineHeight', resolvedModes) || 16
105
- const titleFontFamily =
106
- getVariableByName('listItem/title/fontFamily', resolvedModes) || 'System'
107
- const titleFontWeightRaw =
108
- getVariableByName('listItem/title/fontWeight', resolvedModes) || 700
109
- const titleFontWeight =
110
- typeof titleFontWeightRaw === 'number'
111
- ? titleFontWeightRaw.toString()
112
- : titleFontWeightRaw
113
-
114
- // Support text typography (used in both layouts)
115
- const supportColor =
116
- getVariableByName('listItem/supportText/color', resolvedModes) || '#1f1a14'
117
- const supportFontSize =
118
- getVariableByName('listItem/supportText/fontSize', resolvedModes) || 12
119
- const supportLineHeight =
120
- getVariableByName('listItem/supportText/lineHeight', resolvedModes) || 14
121
- const supportFontFamily =
122
- getVariableByName('listItem/supportText/fontFamily', resolvedModes) || 'System'
123
- const supportFontWeightRaw =
124
- getVariableByName('listItem/supportText/fontWeight', resolvedModes) || 500
125
- const supportFontWeight =
126
- typeof supportFontWeightRaw === 'number'
127
- ? supportFontWeightRaw.toString()
128
- : supportFontWeightRaw
129
-
130
- const baseContainerStyle = {
131
- paddingTop,
132
- paddingBottom,
133
- paddingLeft,
134
- paddingRight,
135
- }
136
-
137
- const sharedLayoutStyle: ViewStyle =
138
- layout === 'Horizontal'
139
- ? {
140
- flexDirection: 'row' as const,
141
- alignItems: 'center' as const,
142
- gap,
143
- }
144
- : {
145
- flexDirection: 'column' as const,
146
- alignItems: 'center' as const,
147
- gap,
148
- }
149
-
150
- const innerContentBaseStyle: ViewStyle = {
151
- flexDirection: 'row',
152
- alignItems: 'center',
153
- flex: 1,
154
- gap,
155
- }
156
-
157
- const verticalContentBaseStyle: ViewStyle = {
158
- alignItems: 'center',
159
- gap,
160
- }
161
-
162
- const titleTextStyle: TextStyle = {
163
- color: titleColor,
164
- fontSize: titleFontSize,
165
- lineHeight: titleLineHeight,
166
- fontFamily: titleFontFamily,
167
- fontWeight: titleFontWeight,
168
- }
169
-
170
- const supportTextStyle: TextStyle = {
171
- color: supportColor,
172
- fontSize: supportFontSize,
173
- lineHeight: supportLineHeight,
174
- fontFamily: supportFontFamily,
175
- fontWeight: supportFontWeight,
176
- }
177
-
178
- const trailingWrapperStyle = useMemo(
179
- () => ({ marginLeft: gap / 2 }),
180
- [gap],
181
- )
212
+ const tokens = useMemo(() => resolveListItemTokens(modes), [modes])
182
213
 
214
+ // Mirror user-supplied handlers in a ref so our wrappers can stay
215
+ // referentially stable. Without this, every parent re-render hands
216
+ // Pressable a fresh function identity for each event prop.
217
+ const userHandlersRef = useRef<{
218
+ onPressIn?: (e: any) => void
219
+ onPressOut?: (e: any) => void
220
+ }>({})
221
+ userHandlersRef.current.onPressIn = (rest as any)?.onPressIn
222
+ userHandlersRef.current.onPressOut = (rest as any)?.onPressOut
183
223
 
184
224
  // Generate default accessibility label from title and supportText if not provided
185
225
  const defaultAccessibilityLabel = accessibilityLabel ||
@@ -187,8 +227,9 @@ function ListItem({
187
227
  ? `${title}${showSupportText && supportText ? `, ${supportText}` : ''}`
188
228
  : supportText)
189
229
 
190
- // Get web platform support props (only used when onPress is defined)
191
- const webPropsHorizontal = usePressableWebSupport({
230
+ // Single web-platform support payload the previous version called
231
+ // `usePressableWebSupport` twice with identical args, which was dead work.
232
+ const webProps = usePressableWebSupport({
192
233
  restProps: rest,
193
234
  onPress,
194
235
  disabled: false,
@@ -196,52 +237,54 @@ function ListItem({
196
237
  webAccessibilityProps,
197
238
  })
198
239
 
199
- const webPropsVertical = usePressableWebSupport({
200
- restProps: rest,
201
- onPress,
202
- disabled: false,
203
- accessibilityLabel: defaultAccessibilityLabel,
204
- webAccessibilityProps,
205
- })
240
+ // Process leading slot to pass modes to children. Memoized on
241
+ // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
242
+ const leadingElement = useMemo(() => {
243
+ const processed = leading
244
+ ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes)
245
+ : []
246
+ if (processed.length === 0) {
247
+ return <IconCapsule modes={tokens.resolvedModes} accessibilityLabel={undefined} />
248
+ }
249
+ return processed.length === 1 ? processed[0] : processed
250
+ }, [leading, tokens.resolvedModes])
206
251
 
207
- // Process leading slot to pass modes to children
208
- const processedLeading = leading
209
- ? cloneChildrenWithModes(React.Children.toArray(leading), resolvedModes)
210
- : []
211
- // Extract single element if array has one element, otherwise use array
212
- const leadingElement =
213
- processedLeading.length > 0
214
- ? processedLeading.length === 1
215
- ? processedLeading[0]
216
- : processedLeading
217
- : <IconCapsule modes={resolvedModes} accessibilityLabel={undefined} />
252
+ const processedSupportSlot = useMemo(() => {
253
+ if (!supportSlot) return null
254
+ const processed = cloneChildrenWithModes(
255
+ React.Children.toArray(supportSlot),
256
+ tokens.resolvedModes
257
+ )
258
+ return processed.length === 1 ? processed[0] : processed
259
+ }, [supportSlot, tokens.resolvedModes])
260
+
261
+ const processedEndSlot = useMemo(() => {
262
+ if (!endSlot) return null
263
+ const processed = cloneChildrenWithModes(
264
+ React.Children.toArray(endSlot),
265
+ tokens.resolvedModes,
266
+ END_SLOT_FORCED_MODES
267
+ )
268
+ return processed.length === 1 ? processed[0] : processed
269
+ }, [endSlot, tokens.resolvedModes])
218
270
 
219
271
  const renderSupportContent = () => {
220
- if (supportSlot) {
221
- // Process supportSlot to pass modes to children
222
- const processedSupportSlot = cloneChildrenWithModes(
223
- React.Children.toArray(supportSlot),
224
- resolvedModes
225
- )
226
- // Extract single element if array has one element, otherwise use array
227
- return processedSupportSlot.length === 1
228
- ? processedSupportSlot[0]
229
- : processedSupportSlot
230
- }
272
+ if (processedSupportSlot) return processedSupportSlot
231
273
 
232
- // Default support text (used for:
233
- // - vertical layout main text
234
- // - horizontal layout secondary line)
274
+ // Default support text:
275
+ // - vertical layout: main text (line-broken on spaces)
276
+ // - horizontal layout: secondary line (clamped to 2 lines)
235
277
  const displayText = layout === 'Vertical'
236
278
  ? supportText.replace(/ /g, '\n')
237
279
  : supportText
238
280
 
239
281
  return (
240
282
  <Text
241
- style={[
242
- supportTextStyle,
243
- layout === 'Vertical' ? { textAlign: 'center' } : undefined,
244
- ]}
283
+ style={
284
+ layout === 'Vertical'
285
+ ? [tokens.supportTextStyle, verticalSupportTextOverride]
286
+ : tokens.supportTextStyle
287
+ }
245
288
  numberOfLines={layout === 'Horizontal' ? 2 : undefined}
246
289
  >
247
290
  {displayText}
@@ -249,44 +292,77 @@ function ListItem({
249
292
  )
250
293
  }
251
294
 
252
- if (layout === 'Horizontal') {
253
- // Process endSlot to pass modes to children
254
- // Force Context: 'ListItem' - this value can never be overridden by external modes
255
- const processedEndSlot = endSlot
256
- ? cloneChildrenWithModes(React.Children.toArray(endSlot), resolvedModes, { "Context": 'ListItem' })
257
- : []
258
- // Extract single element if array has one element, otherwise use array
259
- const trailingContent =
260
- processedEndSlot.length > 0
261
- ? processedEndSlot.length === 1
262
- ? processedEndSlot[0]
263
- : processedEndSlot
264
- : null
295
+ // Stable handlers user-supplied callbacks are forwarded via ref.
296
+ const handlePressIn = useCallback((e: any) => {
297
+ userHandlersRef.current.onPressIn?.(e)
298
+ }, [])
299
+ const handlePressOut = useCallback((e: any) => {
300
+ userHandlersRef.current.onPressOut?.(e)
301
+ }, [])
302
+
303
+ const horizontalContainerStyleArray = useMemo(
304
+ () => [tokens.baseContainerStyle, tokens.horizontalLayoutStyle, style],
305
+ [tokens.baseContainerStyle, tokens.horizontalLayoutStyle, style]
306
+ )
307
+
308
+ const verticalContainerStyleArray = useMemo(
309
+ () => [tokens.baseContainerStyle, tokens.verticalLayoutStyle, style],
310
+ [tokens.baseContainerStyle, tokens.verticalLayoutStyle, style]
311
+ )
312
+
313
+ const horizontalPressableStyle = useCallback(
314
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
315
+ tokens.baseContainerStyle,
316
+ tokens.horizontalLayoutStyle,
317
+ pressed ? pressedOverlayStyle : null,
318
+ style,
319
+ ],
320
+ [tokens.baseContainerStyle, tokens.horizontalLayoutStyle, style]
321
+ )
322
+
323
+ const verticalPressableStyle = useCallback(
324
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
325
+ tokens.baseContainerStyle,
326
+ tokens.verticalLayoutStyle,
327
+ pressed ? pressedOverlayStyle : null,
328
+ style,
329
+ ],
330
+ [tokens.baseContainerStyle, tokens.verticalLayoutStyle, style]
331
+ )
332
+
333
+ const innerContentStyleArray = useMemo(
334
+ () => [tokens.innerContentBaseStyle, contentStyle],
335
+ [tokens.innerContentBaseStyle, contentStyle]
336
+ )
337
+
338
+ const verticalContentStyleArray = useMemo(
339
+ () => [tokens.verticalContentBaseStyle, contentStyle],
340
+ [tokens.verticalContentBaseStyle, contentStyle]
341
+ )
265
342
 
343
+ if (layout === 'Horizontal') {
266
344
  const innerContent = (
267
- <View
268
- style={[innerContentBaseStyle, contentStyle]}
269
- >
345
+ <View style={innerContentStyleArray}>
270
346
  {leadingElement}
271
347
  <View
272
348
  style={{
273
349
  flex: 1,
274
350
  minWidth: 1,
275
- gap: textWrapGap,
351
+ gap: tokens.textWrapGap,
276
352
  }}
277
353
  >
278
354
  <Text
279
- style={titleTextStyle}
355
+ style={tokens.titleTextStyle}
280
356
  numberOfLines={1}
281
357
  >
282
358
  {title}
283
359
  </Text>
284
360
  {showSupportText && renderSupportContent()}
285
361
  </View>
286
- {trailingContent ? (
287
- <View style={trailingWrapperStyle}>{trailingContent}</View>
362
+ {processedEndSlot ? (
363
+ <View style={tokens.trailingWrapperStyle}>{processedEndSlot}</View>
288
364
  ) : null}
289
- {navArrow && <NavArrow direction="Forward" modes={resolvedModes} />}
365
+ {navArrow && <NavArrow direction="Forward" modes={tokens.resolvedModes} />}
290
366
  </View>
291
367
  )
292
368
 
@@ -296,17 +372,13 @@ function ListItem({
296
372
  accessibilityRole="button"
297
373
  accessibilityLabel={undefined}
298
374
  accessibilityHint={accessibilityHint}
299
- accessibilityState={{
300
- ...accessibilityState
301
- }}
375
+ accessibilityState={accessibilityState}
302
376
  onPress={onPress}
303
- style={({ pressed }) => [
304
- baseContainerStyle,
305
- sharedLayoutStyle,
306
- pressed ? { opacity: 0.85 } : null,
307
- style,
308
- ]}
309
- {...webPropsHorizontal}
377
+ onPressIn={handlePressIn}
378
+ onPressOut={handlePressOut}
379
+ unstable_pressDelay={PRESS_DELAY}
380
+ style={horizontalPressableStyle}
381
+ {...webProps}
310
382
  >
311
383
  {innerContent}
312
384
  </Pressable>
@@ -318,7 +390,7 @@ function ListItem({
318
390
  accessibilityRole={undefined}
319
391
  accessibilityLabel={undefined}
320
392
  accessibilityHint={accessibilityHint}
321
- style={[baseContainerStyle, sharedLayoutStyle, style]}
393
+ style={horizontalContainerStyleArray}
322
394
  {...rest}
323
395
  >
324
396
  {innerContent}
@@ -326,9 +398,9 @@ function ListItem({
326
398
  )
327
399
  }
328
400
 
329
- // Vertical layout - icon on top, support text/slot below
401
+ // Vertical layout icon on top, support text/slot below
330
402
  const verticalContent = (
331
- <View style={[verticalContentBaseStyle, contentStyle]}>
403
+ <View style={verticalContentStyleArray}>
332
404
  {leadingElement}
333
405
  {renderSupportContent()}
334
406
  </View>
@@ -340,17 +412,13 @@ function ListItem({
340
412
  accessibilityRole="button"
341
413
  accessibilityLabel={undefined}
342
414
  accessibilityHint={accessibilityHint}
343
- accessibilityState={{
344
- ...accessibilityState
345
- }}
415
+ accessibilityState={accessibilityState}
346
416
  onPress={onPress}
347
- style={({ pressed }) => [
348
- baseContainerStyle,
349
- sharedLayoutStyle,
350
- pressed ? { opacity: 0.85 } : null,
351
- style,
352
- ]}
353
- {...webPropsVertical}
417
+ onPressIn={handlePressIn}
418
+ onPressOut={handlePressOut}
419
+ unstable_pressDelay={PRESS_DELAY}
420
+ style={verticalPressableStyle}
421
+ {...webProps}
354
422
  >
355
423
  {verticalContent}
356
424
  </Pressable>
@@ -362,7 +430,7 @@ function ListItem({
362
430
  accessibilityRole={undefined}
363
431
  accessibilityLabel={undefined}
364
432
  accessibilityHint={accessibilityHint}
365
- style={[baseContainerStyle, sharedLayoutStyle, style]}
433
+ style={verticalContainerStyleArray}
366
434
  {...rest}
367
435
  >
368
436
  {verticalContent}
@@ -370,4 +438,6 @@ function ListItem({
370
438
  )
371
439
  }
372
440
 
441
+ const ListItem = React.memo(ListItemImpl)
442
+
373
443
  export default ListItem
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp,
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
4
4
  import Button, { type ButtonProps } from '../Button/Button';
5
5
  import Avatar, { type AvatarProps } from '../Avatar/Avatar';
6
+ import { EMPTY_MODES } from '../../utils/react-utils';
6
7
 
7
8
  /**
8
9
  * Context to share 'modes' with child components.
@@ -37,7 +38,7 @@ export interface MediaCardProps {
37
38
  export function MediaCard({
38
39
  media,
39
40
  children,
40
- modes = {},
41
+ modes = EMPTY_MODES,
41
42
  style,
42
43
  }: MediaCardProps) {
43
44
  // Container Tokens
@@ -7,6 +7,7 @@ import {
7
7
  type StyleProp,
8
8
  } from 'react-native'
9
9
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { EMPTY_MODES } from '../../utils/react-utils'
10
11
  import Avatar, { type AvatarProps } from '../Avatar/Avatar'
11
12
 
12
13
  export type MerchantProfileProps = {
@@ -76,7 +77,7 @@ export type MerchantProfileProps = {
76
77
  function MerchantProfile({
77
78
  title = 'Uber India Systems Private Limited',
78
79
  subtitle = 'UPI ID: uberindia@ybl',
79
- modes = {},
80
+ modes = EMPTY_MODES,
80
81
  style,
81
82
  avatarProps,
82
83
  accessibilityLabel,
@@ -9,6 +9,7 @@ import {
9
9
  type TextStyle,
10
10
  } from 'react-native'
11
11
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
+ import { EMPTY_MODES } from '../../utils/react-utils'
12
13
 
13
14
  // Map of common ISO 4217 currency codes to display symbols
14
15
  const CURRENCY_SYMBOLS = {
@@ -83,7 +84,7 @@ function MoneyValue({
83
84
  negative,
84
85
  focused = false,
85
86
  hidden = false,
86
- modes = {},
87
+ modes = EMPTY_MODES,
87
88
  style,
88
89
  valueStyle,
89
90
  currencyStyle,