jfs-components 0.0.61 → 0.0.63

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 (259) hide show
  1. package/CHANGELOG.md +36 -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 +2 -1
  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 +2 -3
  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 +268 -156
  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/JFSThemeProvider.js +2 -38
  79. package/lib/commonjs/design-tokens/figma-variables-resolver.js +21 -3
  80. package/lib/commonjs/icons/registry.js +1 -1
  81. package/lib/commonjs/utils/react-utils.js +26 -3
  82. package/lib/module/components/Accordion/Accordion.js +2 -2
  83. package/lib/module/components/ActionFooter/ActionFooter.js +2 -2
  84. package/lib/module/components/ActionTile/ActionTile.js +2 -1
  85. package/lib/module/components/AmountInput/AmountInput.js +2 -1
  86. package/lib/module/components/AppBar/AppBar.js +2 -2
  87. package/lib/module/components/Avatar/Avatar.js +184 -162
  88. package/lib/module/components/AvatarGroup/AvatarGroup.js +2 -2
  89. package/lib/module/components/Badge/Badge.js +2 -1
  90. package/lib/module/components/Balance/Balance.js +2 -1
  91. package/lib/module/components/BottomNav/BottomNav.js +2 -1
  92. package/lib/module/components/BottomNavItem/BottomNavItem.js +108 -88
  93. package/lib/module/components/Button/Button.js +192 -95
  94. package/lib/module/components/ButtonGroup/ButtonGroup.js +2 -2
  95. package/lib/module/components/Card/Card.js +2 -1
  96. package/lib/module/components/CardCTA/CardCTA.js +2 -2
  97. package/lib/module/components/CardProviderInfo/CardProviderInfo.js +2 -2
  98. package/lib/module/components/Carousel/Carousel.js +3 -2
  99. package/lib/module/components/Checkbox/Checkbox.js +2 -1
  100. package/lib/module/components/ChipGroup/ChipGroup.js +2 -2
  101. package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
  102. package/lib/module/components/DebitCard/DebitCard.js +2 -2
  103. package/lib/module/components/Disclaimer/Disclaimer.js +2 -1
  104. package/lib/module/components/Divider/Divider.js +2 -1
  105. package/lib/module/components/Drawer/Drawer.js +2 -1
  106. package/lib/module/components/EmptyState/EmptyState.js +2 -1
  107. package/lib/module/components/FilterBar/FilterBar.js +2 -2
  108. package/lib/module/components/Form/Form.js +2 -1
  109. package/lib/module/components/FormField/FormField.js +3 -2
  110. package/lib/module/components/HStack/HStack.js +2 -2
  111. package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -1
  112. package/lib/module/components/IconButton/IconButton.js +120 -130
  113. package/lib/module/components/IconCapsule/IconCapsule.js +60 -57
  114. package/lib/module/components/InputSearch/InputSearch.js +7 -3
  115. package/lib/module/components/LazyList/LazyList.js +2 -2
  116. package/lib/module/components/LinearMeter/LinearMeter.js +3 -2
  117. package/lib/module/components/ListGroup/ListGroup.js +3 -4
  118. package/lib/module/components/ListItem/ListItem.js +194 -146
  119. package/lib/module/components/MediaCard/MediaCard.js +4 -2
  120. package/lib/module/components/MerchantProfile/MerchantProfile.js +2 -1
  121. package/lib/module/components/MoneyValue/MoneyValue.js +2 -1
  122. package/lib/module/components/NavArrow/NavArrow.js +82 -58
  123. package/lib/module/components/NoteInput/NoteInput.js +2 -1
  124. package/lib/module/components/Nudge/Nudge.js +2 -2
  125. package/lib/module/components/Numpad/Numpad.js +2 -1
  126. package/lib/module/components/OTP/OTP.js +2 -2
  127. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +2 -1
  128. package/lib/module/components/Popup/Popup.js +2 -1
  129. package/lib/module/components/ProductLabel/ProductLabel.js +2 -1
  130. package/lib/module/components/ProgressBadge/ProgressBadge.js +2 -1
  131. package/lib/module/components/RadioButton/RadioButton.js +2 -1
  132. package/lib/module/components/RechargeCard/RechargeCard.js +2 -1
  133. package/lib/module/components/Screen/Screen.js +2 -2
  134. package/lib/module/components/Section/Section.js +271 -159
  135. package/lib/module/components/SegmentedControl/SegmentedControl.js +3 -2
  136. package/lib/module/components/StatItem/StatItem.js +2 -1
  137. package/lib/module/components/StatusHero/StatusHero.js +2 -1
  138. package/lib/module/components/Stepper/Step.js +2 -1
  139. package/lib/module/components/Stepper/StepLabel.js +2 -1
  140. package/lib/module/components/Stepper/Stepper.js +2 -1
  141. package/lib/module/components/SupportText/SupportText.js +2 -1
  142. package/lib/module/components/SupportText/SupportTextIcon.js +2 -1
  143. package/lib/module/components/SwappableAmount/SwappableAmount.js +2 -1
  144. package/lib/module/components/Tabs/TabItem.js +2 -1
  145. package/lib/module/components/Tabs/Tabs.js +2 -1
  146. package/lib/module/components/Text/Text.js +2 -1
  147. package/lib/module/components/TextInput/TextInput.js +3 -3
  148. package/lib/module/components/ThreadHero/ThreadHero.js +2 -1
  149. package/lib/module/components/Title/Title.js +2 -1
  150. package/lib/module/components/Toast/Toast.js +2 -1
  151. package/lib/module/components/Toggle/Toggle.js +2 -1
  152. package/lib/module/components/Tooltip/Tooltip.js +2 -1
  153. package/lib/module/components/TransactionBubble/TransactionBubble.js +2 -2
  154. package/lib/module/components/TransactionDetails/TransactionDetails.js +3 -3
  155. package/lib/module/components/TransactionStatus/TransactionStatus.js +3 -2
  156. package/lib/module/components/UpiHandle/UpiHandle.js +147 -113
  157. package/lib/module/components/VStack/VStack.js +2 -2
  158. package/lib/module/design-tokens/JFSThemeProvider.js +2 -35
  159. package/lib/module/design-tokens/figma-variables-resolver.js +21 -3
  160. package/lib/module/icons/registry.js +1 -1
  161. package/lib/module/utils/react-utils.js +25 -3
  162. package/lib/typescript/src/components/Avatar/Avatar.d.ts +11 -17
  163. package/lib/typescript/src/components/BottomNavItem/BottomNavItem.d.ts +12 -8
  164. package/lib/typescript/src/components/Button/Button.d.ts +18 -1
  165. package/lib/typescript/src/components/IconButton/IconButton.d.ts +12 -29
  166. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +10 -18
  167. package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +8 -3
  168. package/lib/typescript/src/components/ListItem/ListItem.d.ts +14 -1
  169. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +12 -11
  170. package/lib/typescript/src/components/Section/Section.d.ts +2 -48
  171. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +13 -12
  172. package/lib/typescript/src/design-tokens/JFSThemeProvider.d.ts +0 -15
  173. package/lib/typescript/src/icons/registry.d.ts +1 -1
  174. package/lib/typescript/src/utils/react-utils.d.ts +15 -0
  175. package/package.json +4 -6
  176. package/src/components/Accordion/Accordion.tsx +2 -2
  177. package/src/components/ActionFooter/ActionFooter.tsx +2 -2
  178. package/src/components/ActionTile/ActionTile.tsx +2 -1
  179. package/src/components/AmountInput/AmountInput.tsx +2 -1
  180. package/src/components/AppBar/AppBar.tsx +2 -2
  181. package/src/components/Avatar/Avatar.tsx +229 -158
  182. package/src/components/AvatarGroup/AvatarGroup.tsx +2 -2
  183. package/src/components/Badge/Badge.tsx +2 -1
  184. package/src/components/Balance/Balance.tsx +2 -1
  185. package/src/components/BottomNav/BottomNav.tsx +2 -1
  186. package/src/components/BottomNavItem/BottomNavItem.tsx +159 -88
  187. package/src/components/Button/Button.tsx +228 -101
  188. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  189. package/src/components/Card/Card.tsx +2 -1
  190. package/src/components/CardCTA/CardCTA.tsx +2 -2
  191. package/src/components/CardProviderInfo/CardProviderInfo.tsx +2 -2
  192. package/src/components/Carousel/Carousel.tsx +3 -2
  193. package/src/components/Checkbox/Checkbox.tsx +2 -1
  194. package/src/components/ChipGroup/ChipGroup.tsx +2 -2
  195. package/src/components/ChipSelect/ChipSelect.tsx +2 -1
  196. package/src/components/DebitCard/DebitCard.tsx +2 -2
  197. package/src/components/Disclaimer/Disclaimer.tsx +2 -1
  198. package/src/components/Divider/Divider.tsx +2 -1
  199. package/src/components/Drawer/Drawer.tsx +2 -1
  200. package/src/components/EmptyState/EmptyState.tsx +2 -1
  201. package/src/components/FilterBar/FilterBar.tsx +2 -2
  202. package/src/components/Form/Form.tsx +2 -1
  203. package/src/components/FormField/FormField.tsx +3 -2
  204. package/src/components/HStack/HStack.tsx +2 -2
  205. package/src/components/HoldingsCard/HoldingsCard.tsx +2 -1
  206. package/src/components/IconButton/IconButton.tsx +154 -126
  207. package/src/components/IconCapsule/IconCapsule.tsx +73 -54
  208. package/src/components/InputSearch/InputSearch.tsx +19 -5
  209. package/src/components/LazyList/LazyList.tsx +2 -2
  210. package/src/components/LinearMeter/LinearMeter.tsx +3 -2
  211. package/src/components/ListGroup/ListGroup.tsx +4 -5
  212. package/src/components/ListItem/ListItem.tsx +257 -187
  213. package/src/components/MediaCard/MediaCard.tsx +2 -1
  214. package/src/components/MerchantProfile/MerchantProfile.tsx +2 -1
  215. package/src/components/MoneyValue/MoneyValue.tsx +2 -1
  216. package/src/components/NavArrow/NavArrow.tsx +91 -58
  217. package/src/components/NoteInput/NoteInput.tsx +2 -1
  218. package/src/components/Nudge/Nudge.tsx +2 -2
  219. package/src/components/Numpad/Numpad.tsx +2 -1
  220. package/src/components/OTP/OTP.tsx +2 -2
  221. package/src/components/PaymentFeedback/PaymentFeedback.tsx +2 -1
  222. package/src/components/Popup/Popup.tsx +2 -1
  223. package/src/components/ProductLabel/ProductLabel.tsx +2 -1
  224. package/src/components/ProgressBadge/ProgressBadge.tsx +2 -2
  225. package/src/components/RadioButton/RadioButton.tsx +2 -1
  226. package/src/components/RechargeCard/RechargeCard.tsx +2 -1
  227. package/src/components/Screen/Screen.tsx +2 -2
  228. package/src/components/Section/Section.tsx +323 -167
  229. package/src/components/SegmentedControl/SegmentedControl.tsx +3 -2
  230. package/src/components/StatItem/StatItem.tsx +2 -1
  231. package/src/components/StatusHero/StatusHero.tsx +2 -1
  232. package/src/components/Stepper/Step.tsx +2 -1
  233. package/src/components/Stepper/StepLabel.tsx +2 -1
  234. package/src/components/Stepper/Stepper.tsx +2 -1
  235. package/src/components/SupportText/SupportText.tsx +2 -1
  236. package/src/components/SupportText/SupportTextIcon.tsx +2 -1
  237. package/src/components/SwappableAmount/SwappableAmount.tsx +2 -1
  238. package/src/components/Tabs/TabItem.tsx +2 -1
  239. package/src/components/Tabs/Tabs.tsx +2 -1
  240. package/src/components/Text/Text.tsx +2 -1
  241. package/src/components/TextInput/TextInput.tsx +3 -3
  242. package/src/components/ThreadHero/ThreadHero.tsx +2 -1
  243. package/src/components/Title/Title.tsx +2 -1
  244. package/src/components/Toast/Toast.tsx +2 -1
  245. package/src/components/Toggle/Toggle.tsx +2 -1
  246. package/src/components/Tooltip/Tooltip.tsx +2 -1
  247. package/src/components/TransactionBubble/TransactionBubble.tsx +2 -2
  248. package/src/components/TransactionDetails/TransactionDetails.tsx +3 -3
  249. package/src/components/TransactionStatus/TransactionStatus.tsx +3 -2
  250. package/src/components/UpiHandle/UpiHandle.tsx +193 -125
  251. package/src/components/VStack/VStack.tsx +2 -2
  252. package/src/design-tokens/JFSThemeProvider.tsx +1 -37
  253. package/src/design-tokens/figma-variables-resolver.ts +21 -3
  254. package/src/icons/registry.ts +1 -1
  255. package/src/utils/react-utils.ts +29 -3
  256. package/lib/typescript/App.d.ts +0 -2
  257. package/lib/typescript/index.d.ts +0 -2
  258. package/lib/typescript/metro.config.d.ts +0 -78
  259. package/lib/typescript/react-native.config.d.ts +0 -4
@@ -1,13 +1,50 @@
1
- import React, { useState, useRef, useCallback, useEffect } from 'react'
1
+ import React, { useState, useMemo, useRef, useCallback } from 'react'
2
2
  import { View, Text, Pressable, Platform, type StyleProp, type ViewStyle, type PressableStateCallbackType } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import NavArrow from '../NavArrow/NavArrow'
5
5
  import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
6
- import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
6
+ import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
7
+
8
+ // Match Button: delay the press visual on iOS so a scroll-cancelled touch
9
+ // never applies the "pressed" style. See Button.tsx for the rationale.
10
+ const IS_WEB = Platform.OS === 'web'
11
+ const IS_IOS = Platform.OS === 'ios'
12
+ const HEADER_PRESS_DELAY = IS_IOS ? 130 : 0
13
+
14
+ // Module-scope style constants — never re-allocated per render.
15
+ const headerWrapStyle: ViewStyle = {
16
+ flexDirection: 'row',
17
+ alignItems: 'center',
18
+ justifyContent: 'space-between',
19
+ }
20
+ const headerHoverStyle: ViewStyle = { opacity: 0.95 }
21
+ const headerPressedStyle: ViewStyle = { opacity: 0.85 }
22
+ const headerFocusStyle: ViewStyle = { borderColor: '#222', borderWidth: 1 }
7
23
 
8
24
  // ---------------------------------------------------------------------------
9
- // Shared grid layout: measures widest child, enforces uniform width,
10
- // chunks into fixed rows of up to maxColumns, space-between per row.
25
+ // Shared grid layout: measures the widest child once per item-count, then
26
+ // renders uniform-width cells laid out with `justify-content: space-between`.
27
+ // This preserves three visual invariants regardless of viewport width:
28
+ // 1. The first cell hugs the container's left edge.
29
+ // 2. The last cell hugs the container's right edge.
30
+ // 3. Cells in row N column K align with cells in row N+1 column K
31
+ // (uniform cell width across the whole grid).
32
+ // Pure flex sizing (e.g. `flexBasis: 0; flexGrow: 1`) cannot satisfy (1) and
33
+ // (2) on wide viewports — it distributes extra space inside each cell, which
34
+ // makes the icon+label content drift toward each cell's center and produces
35
+ // visible "dead" margins on the left and right of the grid.
36
+ //
37
+ // To avoid the blank-flash that the previous implementation suffered from
38
+ // (it hid the grid with `opacity: 0` until measurement completed, and reset
39
+ // every measurement when the item count changed):
40
+ // * Cells render at their *natural* widths during measurement instead of
41
+ // being hidden. With `space-between`, the first/last cells already hug
42
+ // the edges; only the column alignment can be off by a few pixels for
43
+ // the single frame between mount and the onLayout callback.
44
+ // * On item-count change (e.g. expand / collapse), we clear the per-cell
45
+ // samples but keep the rendered layout visible — we never blank out.
46
+ // * No 500ms safety timeout is needed because the grid is visible from the
47
+ // first frame.
11
48
  // ---------------------------------------------------------------------------
12
49
  const SLOT_GRID_MAX_COLUMNS = 4
13
50
 
@@ -17,32 +54,61 @@ type SlotGridProps = {
17
54
  maxColumns?: number;
18
55
  }
19
56
 
20
- function SlotGrid({ items, gap, maxColumns = SLOT_GRID_MAX_COLUMNS }: SlotGridProps) {
57
+ const slotGridRowStyle: ViewStyle = {
58
+ flexDirection: 'row',
59
+ justifyContent: 'space-between',
60
+ }
61
+
62
+ const SlotGrid = React.memo(function SlotGrid({
63
+ items,
64
+ gap,
65
+ maxColumns = SLOT_GRID_MAX_COLUMNS,
66
+ }: SlotGridProps) {
67
+ const totalItems = items.length
68
+
21
69
  const [maxItemWidth, setMaxItemWidth] = useState<number | null>(null)
22
- const [measureTimedOut, setMeasureTimedOut] = useState(false)
70
+ // Tracks the item-count that `maxItemWidth` corresponds to. When the
71
+ // current `totalItems` differs, the existing measurement is considered
72
+ // stale and cells fall back to natural widths until remeasurement.
73
+ const [measuredForCount, setMeasuredForCount] = useState(0)
23
74
  const itemWidthsRef = useRef<Map<number, number>>(new Map())
24
- const totalItems = items.length
25
75
 
26
- useEffect(() => {
27
- itemWidthsRef.current.clear()
28
- setMaxItemWidth(null)
29
- setMeasureTimedOut(false)
30
- }, [totalItems])
31
-
32
- useEffect(() => {
33
- if (maxItemWidth !== null) return
34
- const timer = setTimeout(() => setMeasureTimedOut(true), 500)
35
- return () => clearTimeout(timer)
36
- }, [maxItemWidth, totalItems])
37
-
38
- const handleItemLayout = useCallback((index: number, width: number) => {
39
- itemWidthsRef.current.set(index, width)
40
- if (itemWidthsRef.current.size >= totalItems && totalItems > 0) {
41
- setMaxItemWidth(Math.max(...itemWidthsRef.current.values()))
42
- }
43
- }, [totalItems])
44
-
45
- const isMeasured = maxItemWidth !== null
76
+ // Synchronously invalidate per-cell samples when the item count changes
77
+ // (e.g. show more / less). We deliberately do NOT touch `maxItemWidth` or
78
+ // `measuredForCount` state here — flipping them would force an extra render
79
+ // pass; instead we let the count mismatch (`measuredForCount !== totalItems`)
80
+ // gate the use of the stale value, and the next onLayout cycle will publish
81
+ // a fresh `maxItemWidth` for the new count.
82
+ const prevTotalRef = useRef(totalItems)
83
+ if (prevTotalRef.current !== totalItems) {
84
+ prevTotalRef.current = totalItems
85
+ itemWidthsRef.current = new Map()
86
+ }
87
+
88
+ const handleItemLayout = useCallback(
89
+ (index: number, width: number) => {
90
+ const widths = itemWidthsRef.current
91
+ const previous = widths.get(index)
92
+ if (previous !== undefined && Math.abs(previous - width) < 0.5) return
93
+ widths.set(index, width)
94
+ if (widths.size >= totalItems && totalItems > 0) {
95
+ let newMax = 0
96
+ for (const w of widths.values()) {
97
+ if (w > newMax) newMax = w
98
+ }
99
+ setMaxItemWidth((prev) =>
100
+ prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax
101
+ )
102
+ setMeasuredForCount(totalItems)
103
+ }
104
+ },
105
+ [totalItems]
106
+ )
107
+
108
+ const hasFreshMeasurement =
109
+ maxItemWidth !== null && measuredForCount === totalItems
110
+ const cellWidth = hasFreshMeasurement ? maxItemWidth : undefined
111
+
46
112
  const columns = Math.min(maxColumns, totalItems || 1)
47
113
 
48
114
  const rows: React.ReactNode[][] = []
@@ -50,44 +116,59 @@ function SlotGrid({ items, gap, maxColumns = SLOT_GRID_MAX_COLUMNS }: SlotGridPr
50
116
  rows.push(items.slice(i, i + columns))
51
117
  }
52
118
 
119
+ const containerStyle = useMemo<ViewStyle>(() => ({ gap }), [gap])
120
+ const measuredCellStyle = useMemo<ViewStyle | undefined>(
121
+ () => (cellWidth !== undefined ? { width: cellWidth } : undefined),
122
+ [cellWidth]
123
+ )
124
+
53
125
  return (
54
- <View style={{ gap, ...(isMeasured || measureTimedOut ? {} : { opacity: 0 }) }}>
126
+ <View style={containerStyle}>
55
127
  {rows.map((row, rowIndex) => {
56
128
  const spacersNeeded = row.length < columns ? columns - row.length : 0
57
129
  return (
58
- <View
59
- key={rowIndex}
60
- style={{
61
- flexDirection: 'row' as const,
62
- justifyContent: 'space-between' as const,
63
- }}
64
- >
130
+ <View key={rowIndex} style={slotGridRowStyle}>
65
131
  {row.map((child, colIndex) => {
66
132
  const itemIndex = rowIndex * columns + colIndex
67
133
  return (
68
134
  <View
69
135
  key={itemIndex}
70
136
  onLayout={
71
- !isMeasured
72
- ? (e) => handleItemLayout(itemIndex, e.nativeEvent.layout.width)
73
- : undefined
137
+ hasFreshMeasurement
138
+ ? undefined
139
+ : (e) =>
140
+ handleItemLayout(
141
+ itemIndex,
142
+ e.nativeEvent.layout.width
143
+ )
74
144
  }
75
- style={isMeasured ? { width: maxItemWidth } : undefined}
145
+ style={measuredCellStyle}
76
146
  >
77
147
  {child}
78
148
  </View>
79
149
  )
80
150
  })}
81
- {isMeasured &&
151
+ {hasFreshMeasurement &&
82
152
  spacersNeeded > 0 &&
83
153
  Array.from({ length: spacersNeeded }, (_, i) => (
84
- <View key={`spacer-${i}`} style={{ width: maxItemWidth }} />
154
+ <View key={`spacer-${i}`} style={measuredCellStyle} />
85
155
  ))}
86
156
  </View>
87
157
  )
88
158
  })}
89
159
  </View>
90
160
  )
161
+ }, slotGridPropsAreEqual)
162
+
163
+ function slotGridPropsAreEqual(prev: SlotGridProps, next: SlotGridProps) {
164
+ if (prev.gap !== next.gap) return false
165
+ if ((prev.maxColumns ?? SLOT_GRID_MAX_COLUMNS) !== (next.maxColumns ?? SLOT_GRID_MAX_COLUMNS)) return false
166
+ if (prev.items === next.items) return true
167
+ if (prev.items.length !== next.items.length) return false
168
+ for (let i = 0; i < prev.items.length; i++) {
169
+ if (prev.items[i] !== next.items[i]) return false
170
+ }
171
+ return true
91
172
  }
92
173
 
93
174
  type SectionProps = {
@@ -132,40 +213,27 @@ type SectionProps = {
132
213
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section. If not provided, uses title
133
214
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
134
215
  */
135
- function Section({
136
- title = 'Section title',
137
- supportText = 'Section support text',
138
- showSupportText = true,
139
- slot,
140
- slotDirection = 'row',
141
- modes = {},
142
- onPress,
143
- style,
144
- accessibilityLabel,
145
- accessibilityHint,
146
- webAccessibilityProps,
147
- ...rest
148
- }: SectionProps) {
149
- const [isHeaderFocused, setIsHeaderFocused] = useState(false)
150
- const [isHeaderHovered, setIsHeaderHovered] = useState(false)
151
- const [isHeaderPressed, setIsHeaderPressed] = useState(false)
152
- const headerFocusStyle: ViewStyle = isHeaderFocused ? { borderColor: '#222', borderWidth: 1 } : {}
153
- const headerHoverStyle: ViewStyle = isHeaderHovered ? { opacity: 0.95 } : {}
154
- const headerPressedStyle: ViewStyle = isHeaderPressed ? { opacity: 0.85 } : {}
155
- // Resolve section container tokens
216
+ interface SectionTokens {
217
+ containerStyle: ViewStyle;
218
+ headerStyle: ViewStyle;
219
+ titleStyle: ViewStyle;
220
+ supportTextStyle: ViewStyle;
221
+ sectionGap: number;
222
+ slotGap: number;
223
+ }
224
+
225
+ function resolveSectionTokens(modes: Record<string, any>): SectionTokens {
156
226
  const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff'
157
- const sectionGap = getVariableByName('section/gap', modes) || 12
158
- const slotGap = getVariableByName('slot/gap', modes) || 12
227
+ const sectionGap = (getVariableByName('section/gap', modes) || 12) as number
228
+ const slotGap = (getVariableByName('slot/gap', modes) || 12) as number
159
229
  const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12
160
230
  const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16
161
231
  const radius = getVariableByName('section/radius', modes) || 12
162
232
 
163
- // Resolve section header tokens
164
233
  const headerGap = getVariableByName('section/header/gap', modes) || 8
165
234
  const headerPaddingHorizontal = getVariableByName('section/header/padding/horizontal', modes) || 0
166
235
  const headerPaddingVertical = getVariableByName('section/header/padding/vertical', modes) || 0
167
236
 
168
- // Resolve section title tokens
169
237
  const titleColor = getVariableByName('section/title/color', modes) || '#0f0d0a'
170
238
  const titleFontSize = getVariableByName('section/title/fontSize', modes) || 18
171
239
  const titleLineHeight = getVariableByName('section/title/lineHeight', modes) || 20
@@ -176,7 +244,6 @@ function Section({
176
244
  ? titleFontWeightRaw.toString()
177
245
  : titleFontWeightRaw
178
246
 
179
- // Resolve section support text tokens
180
247
  const supportTextColor = getVariableByName('section/supportText/color', modes) || '#1f1a14'
181
248
  const supportTextFontSize = getVariableByName('section/supportText/fontSize', modes) || 14
182
249
  const supportTextLineHeight = getVariableByName('section/supportText/lineHeight', modes) || 18
@@ -187,46 +254,85 @@ function Section({
187
254
  ? supportTextFontWeightRaw.toString()
188
255
  : supportTextFontWeightRaw
189
256
 
190
- const containerStyle = {
191
- backgroundColor,
192
- paddingHorizontal,
193
- paddingVertical,
194
- borderRadius: radius,
195
- gap: sectionGap,
196
- }
197
-
198
- const headerStyle = {
199
- paddingHorizontal: headerPaddingHorizontal,
200
- paddingVertical: headerPaddingVertical,
201
- gap: headerGap,
202
- }
203
-
204
- const headerWrapStyle: ViewStyle = {
205
- flexDirection: 'row' as const,
206
- alignItems: 'center' as const,
207
- justifyContent: 'space-between' as const,
208
- }
209
-
210
- const titleStyle = {
211
- flex: 1,
212
- color: titleColor,
213
- fontSize: titleFontSize,
214
- lineHeight: titleLineHeight,
215
- fontFamily: titleFontFamily,
216
- fontWeight: titleFontWeight,
257
+ return {
258
+ containerStyle: {
259
+ backgroundColor,
260
+ paddingHorizontal,
261
+ paddingVertical,
262
+ borderRadius: radius,
263
+ gap: sectionGap,
264
+ },
265
+ headerStyle: {
266
+ paddingHorizontal: headerPaddingHorizontal,
267
+ paddingVertical: headerPaddingVertical,
268
+ gap: headerGap,
269
+ },
270
+ titleStyle: {
271
+ flex: 1,
272
+ color: titleColor,
273
+ fontSize: titleFontSize,
274
+ lineHeight: titleLineHeight,
275
+ fontFamily: titleFontFamily,
276
+ fontWeight: titleFontWeight,
277
+ } as ViewStyle,
278
+ supportTextStyle: {
279
+ color: supportTextColor,
280
+ fontSize: supportTextFontSize,
281
+ lineHeight: supportTextLineHeight,
282
+ fontFamily: supportTextFontFamily,
283
+ fontWeight: supportTextFontWeight,
284
+ } as ViewStyle,
285
+ sectionGap,
286
+ slotGap,
217
287
  }
288
+ }
218
289
 
219
- const supportTextStyle = {
220
- color: supportTextColor,
221
- fontSize: supportTextFontSize,
222
- lineHeight: supportTextLineHeight,
223
- fontFamily: supportTextFontFamily,
224
- fontWeight: supportTextFontWeight,
225
- }
290
+ function Section({
291
+ title = 'Section title',
292
+ supportText = 'Section support text',
293
+ showSupportText = true,
294
+ slot,
295
+ slotDirection = 'row',
296
+ modes = EMPTY_MODES,
297
+ onPress,
298
+ style,
299
+ // accessibilityLabel is intentionally accepted on the type for API
300
+ // back-compat, but the inner Pressable/View deliberately pass
301
+ // `accessibilityLabel={undefined}` (the title Text carries the label
302
+ // instead). Prefix to satisfy the unused-var lint while keeping the prop
303
+ // shape unchanged.
304
+ accessibilityLabel: _accessibilityLabel,
305
+ accessibilityHint,
306
+ webAccessibilityProps,
307
+ ...rest
308
+ }: SectionProps) {
309
+ // Focus and hover are still mirrored in React because they are visible,
310
+ // sustained states (web-only in practice). The setters are gated so they
311
+ // never fire on native — keeping the component render-free during touch.
312
+ // Press is handled imperatively via the `Pressable` style callback so a
313
+ // scroll-cancelled touch never schedules a React render.
314
+ const [isHeaderFocused, setIsHeaderFocused] = useState(false)
315
+ const [isHeaderHovered, setIsHeaderHovered] = useState(false)
226
316
 
227
- // Generate default accessibility label from title and supportText
228
- const defaultAccessibilityLabel = accessibilityLabel ||
229
- (showSupportText ? `${title}. ${supportText}` : title)
317
+ // Mirror user handlers in a ref so our wrappers can stay referentially
318
+ // stable. Without this, every parent re-render would hand Pressable fresh
319
+ // function identities and re-bind every event.
320
+ const userHandlersRef = useRef<{
321
+ onPressIn?: (e: any) => void
322
+ onPressOut?: (e: any) => void
323
+ onHoverIn?: (e: any) => void
324
+ onHoverOut?: (e: any) => void
325
+ onFocus?: (e: any) => void
326
+ onBlur?: (e: any) => void
327
+ }>({})
328
+ userHandlersRef.current.onPressIn = (rest as any)?.onPressIn
329
+ userHandlersRef.current.onPressOut = (rest as any)?.onPressOut
330
+ userHandlersRef.current.onHoverIn = (rest as any)?.onHoverIn
331
+ userHandlersRef.current.onHoverOut = (rest as any)?.onHoverOut
332
+ userHandlersRef.current.onFocus = (rest as any)?.onFocus
333
+ userHandlersRef.current.onBlur = (rest as any)?.onBlur
334
+
335
+ const tokens = useMemo(() => resolveSectionTokens(modes), [modes])
230
336
 
231
337
  // Get web platform support props (only used when onPress is defined)
232
338
  const webProps = usePressableWebSupport({
@@ -241,7 +347,7 @@ function Section({
241
347
  <>
242
348
  <View style={headerWrapStyle}>
243
349
  <Text
244
- style={titleStyle}
350
+ style={tokens.titleStyle}
245
351
  numberOfLines={1}
246
352
  accessibilityElementsHidden={true}
247
353
  importantForAccessibility="no"
@@ -254,7 +360,7 @@ function Section({
254
360
  </View>
255
361
  {showSupportText && (
256
362
  <Text
257
- style={supportTextStyle}
363
+ style={tokens.supportTextStyle}
258
364
  numberOfLines={2}
259
365
  accessibilityElementsHidden={true}
260
366
  importantForAccessibility="no"
@@ -265,10 +371,53 @@ function Section({
265
371
  </>
266
372
  )
267
373
 
374
+ // Stable handler identities. User handlers are read through the ref so
375
+ // these wrappers don't need new identities each render.
376
+ const handlePressIn = useCallback((e: any) => {
377
+ userHandlersRef.current.onPressIn?.(e)
378
+ }, [])
379
+ const handlePressOut = useCallback((e: any) => {
380
+ userHandlersRef.current.onPressOut?.(e)
381
+ }, [])
382
+ const handleHoverIn = useCallback((e: any) => {
383
+ if (IS_WEB) setIsHeaderHovered(true)
384
+ userHandlersRef.current.onHoverIn?.(e)
385
+ }, [])
386
+ const handleHoverOut = useCallback((e: any) => {
387
+ if (IS_WEB) setIsHeaderHovered(false)
388
+ userHandlersRef.current.onHoverOut?.(e)
389
+ }, [])
390
+ const handleFocus = useCallback((e: any) => {
391
+ if (IS_WEB) setIsHeaderFocused(true)
392
+ userHandlersRef.current.onFocus?.(e)
393
+ }, [])
394
+ const handleBlur = useCallback((e: any) => {
395
+ if (IS_WEB) setIsHeaderFocused(false)
396
+ userHandlersRef.current.onBlur?.(e)
397
+ }, [])
398
+
399
+ // The pressed visual is applied by the host view directly through the
400
+ // Pressable style callback — no React render is scheduled. We still want
401
+ // the (constant) hover style on web so we keep it in the array.
402
+ const headerStyleCallback = useCallback(
403
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
404
+ tokens.headerStyle,
405
+ pressed ? headerPressedStyle : null,
406
+ isHeaderHovered ? headerHoverStyle : null,
407
+ isHeaderFocused ? headerFocusStyle : null,
408
+ ],
409
+ [tokens.headerStyle, isHeaderHovered, isHeaderFocused]
410
+ )
411
+
412
+ const containerStyleArray = useMemo(
413
+ () => [tokens.containerStyle, style],
414
+ [tokens.containerStyle, style]
415
+ )
416
+
268
417
  return (
269
418
  <View
270
- style={[containerStyle, style]}
271
- {...(Platform.OS === 'web' ? { accessibilityRole: 'region' as any } : undefined)}
419
+ style={containerStyleArray}
420
+ {...(IS_WEB ? { accessibilityRole: 'region' as any } : undefined)}
272
421
  accessibilityLabel={undefined}
273
422
  accessibilityHint={accessibilityHint}
274
423
  {...rest}
@@ -277,62 +426,61 @@ function Section({
277
426
  <Pressable
278
427
  accessibilityRole="button"
279
428
  accessibilityLabel={undefined}
280
- accessibilityHint={accessibilityHint || "Opens section details"}
429
+ accessibilityHint={accessibilityHint || 'Opens section details'}
281
430
  onPress={onPress}
282
- onPressIn={(e: any) => {
283
- setIsHeaderPressed(true)
284
- ; (rest as any)?.onPressIn?.(e)
285
- }}
286
- onPressOut={(e: any) => {
287
- setIsHeaderPressed(false)
288
- ; (rest as any)?.onPressOut?.(e)
289
- }}
290
- onFocus={(e: any) => {
291
- setIsHeaderFocused(true)
292
- ; (rest as any)?.onFocus?.(e)
293
- }}
294
- onBlur={(e: any) => {
295
- setIsHeaderFocused(false)
296
- ; (rest as any)?.onBlur?.(e)
297
- }}
298
- onHoverIn={(e: any) => {
299
- setIsHeaderHovered(true)
300
- ; (rest as any)?.onHoverIn?.(e)
301
- }}
302
- onHoverOut={(e: any) => {
303
- setIsHeaderHovered(false)
304
- ; (rest as any)?.onHoverOut?.(e)
305
- }}
306
- style={({ pressed }: PressableStateCallbackType) => [
307
- headerStyle,
308
- pressed ? headerPressedStyle : null,
309
- headerHoverStyle,
310
- headerFocusStyle,
311
- ]}
431
+ onPressIn={handlePressIn}
432
+ onPressOut={handlePressOut}
433
+ onFocus={handleFocus}
434
+ onBlur={handleBlur}
435
+ onHoverIn={handleHoverIn}
436
+ onHoverOut={handleHoverOut}
437
+ unstable_pressDelay={HEADER_PRESS_DELAY}
438
+ style={headerStyleCallback}
312
439
  {...webProps}
313
440
  >
314
441
  {headerContent}
315
442
  </Pressable>
316
443
  ) : (
317
- <View style={headerStyle}>
444
+ <View style={tokens.headerStyle}>
318
445
  {headerContent}
319
446
  </View>
320
447
  )}
321
- {slot && slotDirection === 'row' && (
322
- <SlotGrid
323
- items={cloneChildrenWithModes(flattenChildren(slot), modes)}
324
- gap={sectionGap}
325
- />
326
- )}
327
- {slot && slotDirection === 'column' && (
328
- <View style={{ flexDirection: 'column' as const, gap: slotGap }}>
329
- {cloneChildrenWithModes(flattenChildren(slot), modes)}
330
- </View>
331
- )}
448
+ {slot && <SectionSlot slot={slot} modes={modes} direction={slotDirection} rowGap={tokens.sectionGap} columnGap={tokens.slotGap} />}
332
449
  </View>
333
450
  )
334
451
  }
335
452
 
453
+ type SectionSlotProps = {
454
+ slot: React.ReactNode;
455
+ modes: Record<string, any>;
456
+ direction: 'row' | 'column';
457
+ rowGap: number;
458
+ columnGap: number;
459
+ }
460
+
461
+ /**
462
+ * Internal helper that processes the slot children once per (slot, modes) pair
463
+ * and dispatches to the row (SlotGrid) or column layout. Splitting this out of
464
+ * `Section` lets the parent re-render (e.g. for header press/hover state)
465
+ * without re-walking the slot tree via `cloneChildrenWithModes`.
466
+ */
467
+ function SectionSlot({ slot, modes, direction, rowGap, columnGap }: SectionSlotProps) {
468
+ const processed = useMemo(
469
+ () => cloneChildrenWithModes(flattenChildren(slot), modes),
470
+ [slot, modes]
471
+ )
472
+
473
+ const columnContainerStyle = useMemo<ViewStyle>(
474
+ () => ({ flexDirection: 'column', gap: columnGap }),
475
+ [columnGap]
476
+ )
477
+
478
+ if (direction === 'row') {
479
+ return <SlotGrid items={processed} gap={rowGap} />
480
+ }
481
+ return <View style={columnContainerStyle}>{processed}</View>
482
+ }
483
+
336
484
  type SectionBentoProps = {
337
485
  navSlot?: React.ReactNode;
338
486
  upiSlot?: React.ReactNode;
@@ -368,12 +516,15 @@ type SectionBentoProps = {
368
516
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section
369
517
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
370
518
  */
519
+ const sectionBentoUpiRowStyle: ViewStyle = { flexDirection: 'row', gap: 8 }
520
+
371
521
  function SectionBento({
372
522
  navSlot,
373
523
  upiSlot,
374
- modes = {},
524
+ modes = EMPTY_MODES,
375
525
  style,
376
- accessibilityLabel = undefined,
526
+ // Same rationale as Section: accepted on the type but unused internally.
527
+ accessibilityLabel: _accessibilityLabel,
377
528
  accessibilityHint,
378
529
  ...rest
379
530
  }: SectionBentoProps) {
@@ -384,21 +535,26 @@ function SectionBento({
384
535
  const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16
385
536
  const radius = getVariableByName('section/radius', modes) || 12
386
537
 
387
- const containerStyle = {
388
- backgroundColor,
389
- paddingHorizontal,
390
- paddingVertical,
391
- borderRadius: radius,
392
- gap,
393
- }
538
+ const containerStyle = useMemo<ViewStyle>(
539
+ () => ({
540
+ backgroundColor,
541
+ paddingHorizontal,
542
+ paddingVertical,
543
+ borderRadius: radius,
544
+ gap,
545
+ }),
546
+ [backgroundColor, paddingHorizontal, paddingVertical, radius, gap]
547
+ )
394
548
 
395
- const processedNavSlot = navSlot
396
- ? cloneChildrenWithModes(flattenChildren(navSlot), modes)
397
- : null
549
+ const processedNavSlot = useMemo(
550
+ () => (navSlot ? cloneChildrenWithModes(flattenChildren(navSlot), modes) : null),
551
+ [navSlot, modes]
552
+ )
398
553
 
399
- const processedUpiSlot = upiSlot
400
- ? cloneChildrenWithModes(flattenChildren(upiSlot), modes)
401
- : null
554
+ const processedUpiSlot = useMemo(
555
+ () => (upiSlot ? cloneChildrenWithModes(flattenChildren(upiSlot), modes) : null),
556
+ [upiSlot, modes]
557
+ )
402
558
 
403
559
  return (
404
560
  <View
@@ -415,7 +571,7 @@ function SectionBento({
415
571
  />
416
572
  )}
417
573
  {processedUpiSlot && (
418
- <View style={{ flexDirection: 'row' as const, gap: 8 }}>
574
+ <View style={sectionBentoUpiRowStyle}>
419
575
  {processedUpiSlot}
420
576
  </View>
421
577
  )}
@@ -8,6 +8,7 @@ import {
8
8
  type TextStyle,
9
9
  } from 'react-native'
10
10
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { EMPTY_MODES } from '../../utils/react-utils'
11
12
 
12
13
  export type SegmentedControlItem = {
13
14
  key: React.Key
@@ -33,7 +34,7 @@ function SegmentedControlSegment({
33
34
  label,
34
35
  active,
35
36
  onPress,
36
- modes = {},
37
+ modes = EMPTY_MODES,
37
38
  }: {
38
39
  label: string
39
40
  active: boolean
@@ -117,7 +118,7 @@ function SegmentedControl({
117
118
  selectedKey: controlledSelectedKey,
118
119
  defaultSelectedKey,
119
120
  onSelectionChange,
120
- modes = {},
121
+ modes = EMPTY_MODES,
121
122
  style,
122
123
  }: SegmentedControlProps) {
123
124
  const isControlled = controlledSelectedKey !== undefined