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,83 +1,269 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState, useRef, useCallback, useEffect } from 'react';
3
+ import React, { useState, useMemo, useRef, useCallback } from 'react';
4
4
  import { View, Text, Pressable, Platform } from 'react-native';
5
+ import Animated, { FadeInUp, FadeOutUp, ReduceMotion, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
5
6
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
7
  import NavArrow from '../NavArrow/NavArrow';
8
+ import IconCapsule from '../IconCapsule/IconCapsule';
9
+ import ListItem from '../ListItem/ListItem';
7
10
  import { usePressableWebSupport } from '../../utils/web-platform-utils';
8
- import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
11
+ import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
12
+
13
+ // Match Button: delay the press visual on iOS so a scroll-cancelled touch
14
+ // never applies the "pressed" style. See Button.tsx for the rationale.
15
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
16
+ const IS_WEB = Platform.OS === 'web';
17
+ const IS_IOS = Platform.OS === 'ios';
18
+ const HEADER_PRESS_DELAY = IS_IOS ? 130 : 0;
19
+
20
+ // Module-scope style constants — never re-allocated per render.
21
+ const headerWrapStyle = {
22
+ flexDirection: 'row',
23
+ alignItems: 'center',
24
+ justifyContent: 'space-between'
25
+ };
26
+ const headerHoverStyle = {
27
+ opacity: 0.95
28
+ };
29
+ const headerPressedStyle = {
30
+ opacity: 0.85
31
+ };
32
+ const headerFocusStyle = {
33
+ borderColor: '#222',
34
+ borderWidth: 1
35
+ };
9
36
 
10
37
  // ---------------------------------------------------------------------------
11
- // Shared grid layout: measures widest child, enforces uniform width,
12
- // chunks into fixed rows of up to maxColumns, space-between per row.
38
+ // Shared grid layout first-row-anchored sizing.
39
+ //
40
+ // We measure each cell of the *first row* via `onLayout`, take their max as
41
+ // the canonical cellWidth, then apply that explicit width to every cell in
42
+ // every row. Combined with `justify-content: space-between`, this preserves
43
+ // three visual invariants regardless of viewport width:
44
+ // 1. The first cell hugs the container's left edge.
45
+ // 2. The last cell hugs the container's right edge.
46
+ // 3. Cells in row N column K align with cells in row N+1 column K
47
+ // (uniform cell width across the whole grid).
48
+ //
49
+ // Why first-row-anchored?
50
+ //
51
+ // The first row is *always present* (collapsed and expanded both render it),
52
+ // so the measurement happens exactly once on first mount and stays valid for
53
+ // the lifetime of the SlotGrid — the cellWidth is *stable across toggles*.
54
+ // New cells in row 2+ mount when the user expands, and by that time
55
+ // `cellWidth` is already cached, so their `Animated.View` `entering` cascade
56
+ // is never interrupted by a re-measurement-driven re-render.
57
+ //
58
+ // Why not container-width math (e.g. `(containerWidth - gaps) / columns`)?
59
+ // That makes every cell wide enough to fill its share of the row. On wide
60
+ // viewports each cell becomes much wider than its content (icon + label),
61
+ // the content centers inside its oversized cell, and the visible result is
62
+ // a "dead margin" of empty space on the left and right of the grid. Sizing
63
+ // cells to natural content + `space-between` instead pushes the first cell
64
+ // flush left and the last cell flush right, distributing leftover space as
65
+ // the inter-cell gap.
66
+ //
67
+ // Why not measure every cell?
68
+ // The original implementation did, and `max()` could change when the item
69
+ // count changed (collapsed picks one max, expanded another), producing a
70
+ // visible width jump on toggle. The per-cell remeasurement also forced a
71
+ // re-render in the same React batch as the `entering` animation mounting,
72
+ // which Reanimated treats as a cancellation signal — cells visibly didn't
73
+ // animate. Anchoring to the first row eliminates both costs.
74
+ //
75
+ // First-frame behavior is preserved (no blank-flash): until the first-row
76
+ // `onLayout` fires, cells render at their natural width with `space-between`
77
+ // already laying them out correctly; the only thing that changes after
78
+ // measurement is that subsequent rows snap to the same cellWidth so columns
79
+ // align.
13
80
  // ---------------------------------------------------------------------------
14
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
15
81
  const SLOT_GRID_MAX_COLUMNS = 4;
16
- function SlotGrid({
82
+
83
+ // Beyond this many "extra" cells, additional cells reuse the cap delay so very
84
+ // large grids (16, 32, …) still feel snappy instead of cascading for >1s.
85
+ const SLOT_GRID_STAGGER_CAP = 8;
86
+ const SLOT_GRID_ENTER_STAGGER_MS = 35;
87
+ const SLOT_GRID_EXIT_STAGGER_MS = 20;
88
+ const SLOT_GRID_EXIT_DURATION_MS = 160;
89
+ const slotGridRowFlowStyle = {
90
+ flexDirection: 'row',
91
+ justifyContent: 'space-between'
92
+ };
93
+ const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
17
94
  items,
18
95
  gap,
19
- maxColumns = SLOT_GRID_MAX_COLUMNS
96
+ maxColumns = SLOT_GRID_MAX_COLUMNS,
97
+ animateExtrasFromIndex,
98
+ animateContainerLayout
20
99
  }) {
21
- const [maxItemWidth, setMaxItemWidth] = useState(null);
22
- const [measureTimedOut, setMeasureTimedOut] = useState(false);
23
- const itemWidthsRef = useRef(new Map());
24
100
  const totalItems = items.length;
25
- useEffect(() => {
26
- itemWidthsRef.current.clear();
27
- setMaxItemWidth(null);
28
- setMeasureTimedOut(false);
29
- }, [totalItems]);
30
- useEffect(() => {
31
- if (maxItemWidth !== null) return;
32
- const timer = setTimeout(() => setMeasureTimedOut(true), 500);
33
- return () => clearTimeout(timer);
34
- }, [maxItemWidth, totalItems]);
35
- const handleItemLayout = useCallback((index, width) => {
36
- itemWidthsRef.current.set(index, width);
37
- if (itemWidthsRef.current.size >= totalItems && totalItems > 0) {
38
- setMaxItemWidth(Math.max(...itemWidthsRef.current.values()));
39
- }
40
- }, [totalItems]);
41
- const isMeasured = maxItemWidth !== null;
42
101
  const columns = Math.min(maxColumns, totalItems || 1);
102
+ // Number of cells in the first row. Capped by `columns` (a fully-filled row)
103
+ // and by `totalItems` (e.g. a 1-item grid has a 1-cell first row).
104
+ const firstRowSize = Math.min(columns, totalItems);
105
+
106
+ // First-row width measurement. Only cells whose `itemIndex < firstRowSize`
107
+ // get an `onLayout` callback. Once we have a width sample for each first-row
108
+ // cell, we publish the max and the callbacks become inert — no further
109
+ // measurement happens for the rest of the SlotGrid's lifetime, so toggles
110
+ // never trigger a re-measurement-driven re-render.
111
+ const [firstRowMaxWidth, setFirstRowMaxWidth] = useState(null);
112
+ const firstRowWidthsRef = useRef(new Map());
113
+ const handleFirstRowItemLayout = useCallback((index, width) => {
114
+ const widths = firstRowWidthsRef.current;
115
+ const previous = widths.get(index);
116
+ if (previous !== undefined && Math.abs(previous - width) < 0.5) return;
117
+ widths.set(index, width);
118
+ if (widths.size >= firstRowSize && firstRowSize > 0) {
119
+ let newMax = 0;
120
+ for (const w of widths.values()) {
121
+ if (w > newMax) newMax = w;
122
+ }
123
+ setFirstRowMaxWidth(prev => prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax);
124
+ }
125
+ }, [firstRowSize]);
126
+ const cellWidth = firstRowMaxWidth;
43
127
  const rows = [];
44
128
  for (let i = 0; i < items.length; i += columns) {
45
129
  rows.push(items.slice(i, i + columns));
46
130
  }
47
- return /*#__PURE__*/_jsx(View, {
48
- style: {
49
- gap,
50
- ...(isMeasured || measureTimedOut ? {} : {
51
- opacity: 0
52
- })
53
- },
131
+ const containerStyle = useMemo(() => ({
132
+ gap
133
+ }), [gap]);
134
+ const cellStyle = useMemo(() => cellWidth !== null ? {
135
+ width: cellWidth
136
+ } : undefined, [cellWidth]);
137
+
138
+ // `space-between` distributes any leftover row space as inter-cell gap, so
139
+ // the first cell is flush-left and the last cell is flush-right regardless
140
+ // of whether cellWidth has been measured yet. Identical layout strategy
141
+ // before and after measurement — no first-frame layout shift.
142
+ const rowStyle = slotGridRowFlowStyle;
143
+ const animationsEnabled = animateExtrasFromIndex !== undefined;
144
+ // Resolve the threshold once. When undefined we treat it as
145
+ // Number.POSITIVE_INFINITY so the per-cell branch always picks the plain
146
+ // `<View>` path.
147
+ const extrasThreshold = animationsEnabled ? animateExtrasFromIndex : Number.POSITIVE_INFINITY;
148
+ // Total count of "extra" cells currently rendered. Used to compute the
149
+ // reverse-stagger delay for the exiting animation so that the last cell
150
+ // leaves first.
151
+ const extrasCount = animationsEnabled ? Math.max(0, totalItems - extrasThreshold) : 0;
152
+ const useAnimatedContainer = animateContainerLayout === true;
153
+
154
+ // Explicit-height clip animation:
155
+ //
156
+ // Reanimated's `LinearTransition` interpolates the container's bounds, and
157
+ // in practice that interpolation drags on the cells inside (they momentarily
158
+ // appear squashed because the parent is shorter than its natural content
159
+ // for the duration of the animation). To keep cells at their *natural size
160
+ // throughout*, we instead:
161
+ // 1. Render the rows inside an inner `<View>` that sizes to its content
162
+ // naturally — cells are never squeezed, never resized.
163
+ // 2. Wrap that inner view in an `Animated.View` with `overflow: 'hidden'`
164
+ // and an explicit `height` driven by a shared value.
165
+ // 3. The inner view reports its natural height via `onLayout`. The first
166
+ // measurement snaps the shared value (no first-mount animation). Every
167
+ // subsequent change (e.g. expand/collapse adds or removes rows) springs
168
+ // the shared value to the new natural height.
169
+ //
170
+ // Visually: the container reveals/conceals content like a curtain, and the
171
+ // cells never deform.
172
+ const animatedHeight = useSharedValue(-1);
173
+ const isFirstHeightLayoutRef = useRef(true);
174
+ const handleContentLayout = useCallback(e => {
175
+ const h = e.nativeEvent.layout.height;
176
+ if (h <= 0) return;
177
+ if (isFirstHeightLayoutRef.current) {
178
+ isFirstHeightLayoutRef.current = false;
179
+ animatedHeight.value = h;
180
+ return;
181
+ }
182
+ animatedHeight.value = withSpring(h, {
183
+ damping: 22,
184
+ stiffness: 180,
185
+ reduceMotion: ReduceMotion.System
186
+ });
187
+ }, [animatedHeight]);
188
+ const animatedHeightStyle = useAnimatedStyle(() => animatedHeight.value < 0 ? {} : {
189
+ height: animatedHeight.value,
190
+ overflow: 'hidden'
191
+ });
192
+ const rowsChildren = /*#__PURE__*/_jsx(_Fragment, {
54
193
  children: rows.map((row, rowIndex) => {
55
194
  const spacersNeeded = row.length < columns ? columns - row.length : 0;
56
195
  return /*#__PURE__*/_jsxs(View, {
57
- style: {
58
- flexDirection: 'row',
59
- justifyContent: 'space-between'
60
- },
196
+ style: rowStyle,
61
197
  children: [row.map((child, colIndex) => {
62
198
  const itemIndex = rowIndex * columns + colIndex;
199
+ // Only first-row cells participate in measurement, and only
200
+ // until firstRowMaxWidth has been published. After that the
201
+ // onLayout becomes a no-op (we elide it).
202
+ const onLayoutHandler = firstRowMaxWidth === null && itemIndex < firstRowSize ? e => handleFirstRowItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined;
203
+ if (itemIndex >= extrasThreshold) {
204
+ const extraOrdinal = itemIndex - extrasThreshold;
205
+ const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
206
+ const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
207
+ const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
208
+ const entering = FadeInUp.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(ReduceMotion.System);
209
+ const exiting = FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(ReduceMotion.System);
210
+ return /*#__PURE__*/_jsx(Animated.View, {
211
+ entering: entering,
212
+ exiting: exiting,
213
+ ...(onLayoutHandler ? {
214
+ onLayout: onLayoutHandler
215
+ } : null),
216
+ style: cellStyle,
217
+ children: child
218
+ }, itemIndex);
219
+ }
63
220
  return /*#__PURE__*/_jsx(View, {
64
- onLayout: !isMeasured ? e => handleItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined,
65
- style: isMeasured ? {
66
- width: maxItemWidth
67
- } : undefined,
221
+ ...(onLayoutHandler ? {
222
+ onLayout: onLayoutHandler
223
+ } : null),
224
+ style: cellStyle,
68
225
  children: child
69
226
  }, itemIndex);
70
- }), isMeasured && spacersNeeded > 0 && Array.from({
227
+ }), cellWidth !== null && spacersNeeded > 0 && Array.from({
71
228
  length: spacersNeeded
72
229
  }, (_, i) => /*#__PURE__*/_jsx(View, {
73
- style: {
74
- width: maxItemWidth
75
- }
230
+ style: cellStyle
76
231
  }, `spacer-${i}`))]
77
232
  }, rowIndex);
78
233
  })
79
234
  });
235
+ if (useAnimatedContainer) {
236
+ // Outer Animated.View clips and animates height. Inner View holds the
237
+ // rows at natural size and reports its natural height for the spring
238
+ // target. Cell-width measurement happens on the cells themselves
239
+ // (first-row only) — no container-level onLayout is needed.
240
+ return /*#__PURE__*/_jsx(Animated.View, {
241
+ style: animatedHeightStyle,
242
+ children: /*#__PURE__*/_jsx(View, {
243
+ style: containerStyle,
244
+ onLayout: handleContentLayout,
245
+ children: rowsChildren
246
+ })
247
+ });
248
+ }
249
+ return /*#__PURE__*/_jsx(View, {
250
+ style: containerStyle,
251
+ children: rowsChildren
252
+ });
253
+ }, slotGridPropsAreEqual);
254
+ function slotGridPropsAreEqual(prev, next) {
255
+ if (prev.gap !== next.gap) return false;
256
+ if ((prev.maxColumns ?? SLOT_GRID_MAX_COLUMNS) !== (next.maxColumns ?? SLOT_GRID_MAX_COLUMNS)) return false;
257
+ if (prev.animateExtrasFromIndex !== next.animateExtrasFromIndex) return false;
258
+ if (prev.animateContainerLayout !== next.animateContainerLayout) return false;
259
+ if (prev.items === next.items) return true;
260
+ if (prev.items.length !== next.items.length) return false;
261
+ for (let i = 0; i < prev.items.length; i++) {
262
+ if (prev.items[i] !== next.items[i]) return false;
263
+ }
264
+ return true;
80
265
  }
266
+
81
267
  /**
82
268
  * Section component that mirrors the Figma "Section" component.
83
269
  *
@@ -102,96 +288,99 @@ function SlotGrid({
102
288
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section. If not provided, uses title
103
289
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
104
290
  */
105
- function Section({
106
- title = 'Section title',
107
- supportText = 'Section support text',
108
- showSupportText = true,
109
- slot,
110
- slotDirection = 'row',
111
- modes = {},
112
- onPress,
113
- style,
114
- accessibilityLabel,
115
- accessibilityHint,
116
- webAccessibilityProps,
117
- ...rest
118
- }) {
119
- const [isHeaderFocused, setIsHeaderFocused] = useState(false);
120
- const [isHeaderHovered, setIsHeaderHovered] = useState(false);
121
- const [isHeaderPressed, setIsHeaderPressed] = useState(false);
122
- const headerFocusStyle = isHeaderFocused ? {
123
- borderColor: '#222',
124
- borderWidth: 1
125
- } : {};
126
- const headerHoverStyle = isHeaderHovered ? {
127
- opacity: 0.95
128
- } : {};
129
- const headerPressedStyle = isHeaderPressed ? {
130
- opacity: 0.85
131
- } : {};
132
- // Resolve section container tokens
291
+
292
+ function resolveSectionTokens(modes) {
133
293
  const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff';
134
294
  const sectionGap = getVariableByName('section/gap', modes) || 12;
135
295
  const slotGap = getVariableByName('slot/gap', modes) || 12;
136
296
  const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12;
137
297
  const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16;
138
298
  const radius = getVariableByName('section/radius', modes) || 12;
139
-
140
- // Resolve section header tokens
141
299
  const headerGap = getVariableByName('section/header/gap', modes) || 8;
142
300
  const headerPaddingHorizontal = getVariableByName('section/header/padding/horizontal', modes) || 0;
143
301
  const headerPaddingVertical = getVariableByName('section/header/padding/vertical', modes) || 0;
144
-
145
- // Resolve section title tokens
146
302
  const titleColor = getVariableByName('section/title/color', modes) || '#0f0d0a';
147
303
  const titleFontSize = getVariableByName('section/title/fontSize', modes) || 18;
148
304
  const titleLineHeight = getVariableByName('section/title/lineHeight', modes) || 20;
149
305
  const titleFontFamily = getVariableByName('section/title/fontFamily', modes) || 'System';
150
306
  const titleFontWeightRaw = getVariableByName('section/title/fontWeight', modes) || 800;
151
307
  const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
152
-
153
- // Resolve section support text tokens
154
308
  const supportTextColor = getVariableByName('section/supportText/color', modes) || '#1f1a14';
155
309
  const supportTextFontSize = getVariableByName('section/supportText/fontSize', modes) || 14;
156
310
  const supportTextLineHeight = getVariableByName('section/supportText/lineHeight', modes) || 18;
157
311
  const supportTextFontFamily = getVariableByName('section/supportText/fontFamily', modes) || 'System';
158
312
  const supportTextFontWeightRaw = getVariableByName('section/supportText/fontWeight', modes) || 500;
159
313
  const supportTextFontWeight = typeof supportTextFontWeightRaw === 'number' ? supportTextFontWeightRaw.toString() : supportTextFontWeightRaw;
160
- const containerStyle = {
161
- backgroundColor,
162
- paddingHorizontal,
163
- paddingVertical,
164
- borderRadius: radius,
165
- gap: sectionGap
166
- };
167
- const headerStyle = {
168
- paddingHorizontal: headerPaddingHorizontal,
169
- paddingVertical: headerPaddingVertical,
170
- gap: headerGap
171
- };
172
- const headerWrapStyle = {
173
- flexDirection: 'row',
174
- alignItems: 'center',
175
- justifyContent: 'space-between'
176
- };
177
- const titleStyle = {
178
- flex: 1,
179
- color: titleColor,
180
- fontSize: titleFontSize,
181
- lineHeight: titleLineHeight,
182
- fontFamily: titleFontFamily,
183
- fontWeight: titleFontWeight
184
- };
185
- const supportTextStyle = {
186
- color: supportTextColor,
187
- fontSize: supportTextFontSize,
188
- lineHeight: supportTextLineHeight,
189
- fontFamily: supportTextFontFamily,
190
- fontWeight: supportTextFontWeight
314
+ return {
315
+ containerStyle: {
316
+ backgroundColor,
317
+ paddingHorizontal,
318
+ paddingVertical,
319
+ borderRadius: radius,
320
+ gap: sectionGap
321
+ },
322
+ headerStyle: {
323
+ paddingHorizontal: headerPaddingHorizontal,
324
+ paddingVertical: headerPaddingVertical,
325
+ gap: headerGap
326
+ },
327
+ titleStyle: {
328
+ flex: 1,
329
+ color: titleColor,
330
+ fontSize: titleFontSize,
331
+ lineHeight: titleLineHeight,
332
+ fontFamily: titleFontFamily,
333
+ fontWeight: titleFontWeight
334
+ },
335
+ supportTextStyle: {
336
+ color: supportTextColor,
337
+ fontSize: supportTextFontSize,
338
+ lineHeight: supportTextLineHeight,
339
+ fontFamily: supportTextFontFamily,
340
+ fontWeight: supportTextFontWeight
341
+ },
342
+ sectionGap,
343
+ slotGap
191
344
  };
345
+ }
346
+ function Section({
347
+ title = 'Section title',
348
+ supportText = 'Section support text',
349
+ showSupportText = true,
350
+ slot,
351
+ slotDirection = 'row',
352
+ modes = EMPTY_MODES,
353
+ onPress,
354
+ style,
355
+ // accessibilityLabel is intentionally accepted on the type for API
356
+ // back-compat, but the inner Pressable/View deliberately pass
357
+ // `accessibilityLabel={undefined}` (the title Text carries the label
358
+ // instead). Prefix to satisfy the unused-var lint while keeping the prop
359
+ // shape unchanged.
360
+ accessibilityLabel: _accessibilityLabel,
361
+ accessibilityHint,
362
+ webAccessibilityProps,
363
+ ...rest
364
+ }) {
365
+ // Focus and hover are still mirrored in React because they are visible,
366
+ // sustained states (web-only in practice). The setters are gated so they
367
+ // never fire on native — keeping the component render-free during touch.
368
+ // Press is handled imperatively via the `Pressable` style callback so a
369
+ // scroll-cancelled touch never schedules a React render.
370
+ const [isHeaderFocused, setIsHeaderFocused] = useState(false);
371
+ const [isHeaderHovered, setIsHeaderHovered] = useState(false);
192
372
 
193
- // Generate default accessibility label from title and supportText
194
- const defaultAccessibilityLabel = accessibilityLabel || (showSupportText ? `${title}. ${supportText}` : title);
373
+ // Mirror user handlers in a ref so our wrappers can stay referentially
374
+ // stable. Without this, every parent re-render would hand Pressable fresh
375
+ // function identities and re-bind every event.
376
+ const userHandlersRef = useRef({});
377
+ userHandlersRef.current.onPressIn = rest?.onPressIn;
378
+ userHandlersRef.current.onPressOut = rest?.onPressOut;
379
+ userHandlersRef.current.onHoverIn = rest?.onHoverIn;
380
+ userHandlersRef.current.onHoverOut = rest?.onHoverOut;
381
+ userHandlersRef.current.onFocus = rest?.onFocus;
382
+ userHandlersRef.current.onBlur = rest?.onBlur;
383
+ const tokens = useMemo(() => resolveSectionTokens(modes), [modes]);
195
384
 
196
385
  // Get web platform support props (only used when onPress is defined)
197
386
  const webProps = usePressableWebSupport({
@@ -205,7 +394,7 @@ function Section({
205
394
  children: [/*#__PURE__*/_jsxs(View, {
206
395
  style: headerWrapStyle,
207
396
  children: [/*#__PURE__*/_jsx(Text, {
208
- style: titleStyle,
397
+ style: tokens.titleStyle,
209
398
  numberOfLines: 1,
210
399
  accessibilityElementsHidden: true,
211
400
  importantForAccessibility: "no",
@@ -215,16 +404,49 @@ function Section({
215
404
  modes: modes
216
405
  })]
217
406
  }), showSupportText && /*#__PURE__*/_jsx(Text, {
218
- style: supportTextStyle,
407
+ style: tokens.supportTextStyle,
219
408
  numberOfLines: 2,
220
409
  accessibilityElementsHidden: true,
221
410
  importantForAccessibility: "no",
222
411
  children: supportText
223
412
  })]
224
413
  });
414
+
415
+ // Stable handler identities. User handlers are read through the ref so
416
+ // these wrappers don't need new identities each render.
417
+ const handlePressIn = useCallback(e => {
418
+ userHandlersRef.current.onPressIn?.(e);
419
+ }, []);
420
+ const handlePressOut = useCallback(e => {
421
+ userHandlersRef.current.onPressOut?.(e);
422
+ }, []);
423
+ const handleHoverIn = useCallback(e => {
424
+ if (IS_WEB) setIsHeaderHovered(true);
425
+ userHandlersRef.current.onHoverIn?.(e);
426
+ }, []);
427
+ const handleHoverOut = useCallback(e => {
428
+ if (IS_WEB) setIsHeaderHovered(false);
429
+ userHandlersRef.current.onHoverOut?.(e);
430
+ }, []);
431
+ const handleFocus = useCallback(e => {
432
+ if (IS_WEB) setIsHeaderFocused(true);
433
+ userHandlersRef.current.onFocus?.(e);
434
+ }, []);
435
+ const handleBlur = useCallback(e => {
436
+ if (IS_WEB) setIsHeaderFocused(false);
437
+ userHandlersRef.current.onBlur?.(e);
438
+ }, []);
439
+
440
+ // The pressed visual is applied by the host view directly through the
441
+ // Pressable style callback — no React render is scheduled. We still want
442
+ // the (constant) hover style on web so we keep it in the array.
443
+ const headerStyleCallback = useCallback(({
444
+ pressed
445
+ }) => [tokens.headerStyle, pressed ? headerPressedStyle : null, isHeaderHovered ? headerHoverStyle : null, isHeaderFocused ? headerFocusStyle : null], [tokens.headerStyle, isHeaderHovered, isHeaderFocused]);
446
+ const containerStyleArray = useMemo(() => [tokens.containerStyle, style], [tokens.containerStyle, style]);
225
447
  return /*#__PURE__*/_jsxs(View, {
226
- style: [containerStyle, style],
227
- ...(Platform.OS === 'web' ? {
448
+ style: containerStyleArray,
449
+ ...(IS_WEB ? {
228
450
  accessibilityRole: 'region'
229
451
  } : undefined),
230
452
  accessibilityLabel: undefined,
@@ -233,52 +455,59 @@ function Section({
233
455
  children: [onPress ? /*#__PURE__*/_jsx(Pressable, {
234
456
  accessibilityRole: "button",
235
457
  accessibilityLabel: undefined,
236
- accessibilityHint: accessibilityHint || "Opens section details",
458
+ accessibilityHint: accessibilityHint || 'Opens section details',
237
459
  onPress: onPress,
238
- onPressIn: e => {
239
- setIsHeaderPressed(true);
240
- rest?.onPressIn?.(e);
241
- },
242
- onPressOut: e => {
243
- setIsHeaderPressed(false);
244
- rest?.onPressOut?.(e);
245
- },
246
- onFocus: e => {
247
- setIsHeaderFocused(true);
248
- rest?.onFocus?.(e);
249
- },
250
- onBlur: e => {
251
- setIsHeaderFocused(false);
252
- rest?.onBlur?.(e);
253
- },
254
- onHoverIn: e => {
255
- setIsHeaderHovered(true);
256
- rest?.onHoverIn?.(e);
257
- },
258
- onHoverOut: e => {
259
- setIsHeaderHovered(false);
260
- rest?.onHoverOut?.(e);
261
- },
262
- style: ({
263
- pressed
264
- }) => [headerStyle, pressed ? headerPressedStyle : null, headerHoverStyle, headerFocusStyle],
460
+ onPressIn: handlePressIn,
461
+ onPressOut: handlePressOut,
462
+ onFocus: handleFocus,
463
+ onBlur: handleBlur,
464
+ onHoverIn: handleHoverIn,
465
+ onHoverOut: handleHoverOut,
466
+ unstable_pressDelay: HEADER_PRESS_DELAY,
467
+ style: headerStyleCallback,
265
468
  ...webProps,
266
469
  children: headerContent
267
470
  }) : /*#__PURE__*/_jsx(View, {
268
- style: headerStyle,
471
+ style: tokens.headerStyle,
269
472
  children: headerContent
270
- }), slot && slotDirection === 'row' && /*#__PURE__*/_jsx(SlotGrid, {
271
- items: cloneChildrenWithModes(flattenChildren(slot), modes),
272
- gap: sectionGap
273
- }), slot && slotDirection === 'column' && /*#__PURE__*/_jsx(View, {
274
- style: {
275
- flexDirection: 'column',
276
- gap: slotGap
277
- },
278
- children: cloneChildrenWithModes(flattenChildren(slot), modes)
473
+ }), slot && /*#__PURE__*/_jsx(SectionSlot, {
474
+ slot: slot,
475
+ modes: modes,
476
+ direction: slotDirection,
477
+ rowGap: tokens.sectionGap,
478
+ columnGap: tokens.slotGap
279
479
  })]
280
480
  });
281
481
  }
482
+ /**
483
+ * Internal helper that processes the slot children once per (slot, modes) pair
484
+ * and dispatches to the row (SlotGrid) or column layout. Splitting this out of
485
+ * `Section` lets the parent re-render (e.g. for header press/hover state)
486
+ * without re-walking the slot tree via `cloneChildrenWithModes`.
487
+ */
488
+ function SectionSlot({
489
+ slot,
490
+ modes,
491
+ direction,
492
+ rowGap,
493
+ columnGap
494
+ }) {
495
+ const processed = useMemo(() => cloneChildrenWithModes(flattenChildren(slot), modes), [slot, modes]);
496
+ const columnContainerStyle = useMemo(() => ({
497
+ flexDirection: 'column',
498
+ gap: columnGap
499
+ }), [columnGap]);
500
+ if (direction === 'row') {
501
+ return /*#__PURE__*/_jsx(SlotGrid, {
502
+ items: processed,
503
+ gap: rowGap
504
+ });
505
+ }
506
+ return /*#__PURE__*/_jsx(View, {
507
+ style: columnContainerStyle,
508
+ children: processed
509
+ });
510
+ }
282
511
  /**
283
512
  * Section.Bento component that mirrors the Figma "Section.Bento" component.
284
513
  *
@@ -301,30 +530,88 @@ function Section({
301
530
  * @param {string} [props.accessibilityLabel] - Accessibility label for the section
302
531
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
303
532
  */
533
+ const sectionBentoUpiRowStyle = {
534
+ flexDirection: 'row',
535
+ gap: 8
536
+ };
304
537
  function SectionBento({
305
538
  navSlot,
306
539
  upiSlot,
307
- modes = {},
540
+ modes = EMPTY_MODES,
308
541
  style,
309
- accessibilityLabel = undefined,
542
+ // Same rationale as Section: accepted on the type but unused internally.
543
+ accessibilityLabel: _accessibilityLabel,
310
544
  accessibilityHint,
545
+ collapsedCount = SLOT_GRID_MAX_COLUMNS,
546
+ defaultExpanded = false,
547
+ expanded: controlledExpanded,
548
+ onExpandedChange,
549
+ toggleMoreLabel = 'More',
550
+ toggleLessLabel = 'Less',
551
+ toggleMoreIcon = 'ic_chevron_down',
552
+ toggleLessIcon = 'ic_chevron_up',
553
+ renderToggle,
311
554
  ...rest
312
555
  }) {
313
- // Resolve section container tokens
314
556
  const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff';
315
557
  const gap = getVariableByName('section/gap', modes) || 12;
316
558
  const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12;
317
559
  const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16;
318
560
  const radius = getVariableByName('section/radius', modes) || 12;
319
- const containerStyle = {
561
+ const containerStyle = useMemo(() => ({
320
562
  backgroundColor,
321
563
  paddingHorizontal,
322
564
  paddingVertical,
323
565
  borderRadius: radius,
324
566
  gap
325
- };
326
- const processedNavSlot = navSlot ? cloneChildrenWithModes(flattenChildren(navSlot), modes) : null;
327
- const processedUpiSlot = upiSlot ? cloneChildrenWithModes(flattenChildren(upiSlot), modes) : null;
567
+ }), [backgroundColor, paddingHorizontal, paddingVertical, radius, gap]);
568
+ const processedNavSlot = useMemo(() => navSlot ? cloneChildrenWithModes(flattenChildren(navSlot), modes) : null, [navSlot, modes]);
569
+ const processedUpiSlot = useMemo(() => upiSlot ? cloneChildrenWithModes(flattenChildren(upiSlot), modes) : null, [upiSlot, modes]);
570
+
571
+ // `canExpand` is true iff there are strictly more real items than fit into
572
+ // the collapsed grid. When `allRealItems.length === collapsedCount` we just
573
+ // render them all with no toggle — identical to the pre-feature behavior.
574
+ const allRealItems = useMemo(() => processedNavSlot ?? [], [processedNavSlot]);
575
+ const canExpand = allRealItems.length > collapsedCount;
576
+ const isControlled = controlledExpanded !== undefined;
577
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
578
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
579
+
580
+ // Mirror onExpandedChange in a ref so `toggle` can stay referentially stable.
581
+ const onExpandedChangeRef = useRef(onExpandedChange);
582
+ onExpandedChangeRef.current = onExpandedChange;
583
+ const isControlledRef = useRef(isControlled);
584
+ isControlledRef.current = isControlled;
585
+ const expandedRef = useRef(expanded);
586
+ expandedRef.current = expanded;
587
+ const toggle = useCallback(() => {
588
+ const next = !expandedRef.current;
589
+ if (!isControlledRef.current) {
590
+ setInternalExpanded(next);
591
+ }
592
+ onExpandedChangeRef.current?.(next);
593
+ }, []);
594
+ const navGridItems = useMemo(() => {
595
+ if (!canExpand) {
596
+ return allRealItems;
597
+ }
598
+ // Leave the last collapsed slot for the toggle cell.
599
+ const visibleRealItems = expanded ? allRealItems : allRealItems.slice(0, collapsedCount - 1);
600
+ const toggleNode = renderToggle ? renderToggle({
601
+ expanded,
602
+ toggle
603
+ }) : /*#__PURE__*/_jsx(DefaultBentoToggle, {
604
+ expanded: expanded,
605
+ onPress: toggle,
606
+ modes: modes,
607
+ moreLabel: toggleMoreLabel,
608
+ lessLabel: toggleLessLabel,
609
+ moreIcon: toggleMoreIcon,
610
+ lessIcon: toggleLessIcon,
611
+ extraCount: allRealItems.length - (collapsedCount - 1)
612
+ });
613
+ return [...visibleRealItems, toggleNode];
614
+ }, [canExpand, allRealItems, expanded, collapsedCount, renderToggle, toggle, modes, toggleMoreLabel, toggleLessLabel, toggleMoreIcon, toggleLessIcon]);
328
615
  return /*#__PURE__*/_jsxs(View, {
329
616
  style: [containerStyle, style],
330
617
  ...(Platform.OS === 'web' ? {
@@ -333,19 +620,66 @@ function SectionBento({
333
620
  accessibilityLabel: undefined,
334
621
  accessibilityHint: accessibilityHint,
335
622
  ...rest,
336
- children: [processedNavSlot && /*#__PURE__*/_jsx(SlotGrid, {
337
- items: processedNavSlot,
338
- gap: gap
623
+ children: [navGridItems.length > 0 && /*#__PURE__*/_jsx(SlotGrid, {
624
+ items: navGridItems,
625
+ gap: gap,
626
+ ...(canExpand ? {
627
+ animateExtrasFromIndex: collapsedCount,
628
+ animateContainerLayout: true
629
+ } : null)
339
630
  }), processedUpiSlot && /*#__PURE__*/_jsx(View, {
340
- style: {
341
- flexDirection: 'row',
342
- gap: 8
343
- },
631
+ style: sectionBentoUpiRowStyle,
344
632
  children: processedUpiSlot
345
633
  })]
346
634
  });
347
635
  }
348
636
 
637
+ // ---------------------------------------------------------------------------
638
+ // DefaultBentoToggle — internal toggle cell rendered by `SectionBento` when no
639
+ // `renderToggle` prop is provided. Uses a vertical `ListItem` so the cell
640
+ // matches the visual rhythm of the surrounding nav items in every mode.
641
+ //
642
+ // Two icons (`ic_chevron_down` / `ic_chevron_up`) are used instead of a
643
+ // rotating single icon, because the toggle cell is reconciled by position
644
+ // (collapsed: end of row 1; expanded: end of last row), so any persistent
645
+ // shared-value-driven rotation would lose its anchor across toggles.
646
+ // ---------------------------------------------------------------------------
647
+
648
+ function DefaultBentoToggle({
649
+ expanded,
650
+ onPress,
651
+ modes,
652
+ moreLabel,
653
+ lessLabel,
654
+ moreIcon,
655
+ lessIcon,
656
+ extraCount
657
+ }) {
658
+ const label = expanded ? lessLabel : moreLabel;
659
+ const iconName = expanded ? lessIcon : moreIcon;
660
+ const accessibilityState = useMemo(() => ({
661
+ expanded
662
+ }), [expanded]);
663
+ const webAccessibilityProps = useMemo(() => ({
664
+ ariaExpanded: expanded
665
+ }), [expanded]);
666
+ const accessibilityHint = expanded ? `Hides ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}` : `Shows ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}`;
667
+ return /*#__PURE__*/_jsx(ListItem, {
668
+ layout: "Vertical",
669
+ supportText: label,
670
+ leading: /*#__PURE__*/_jsx(IconCapsule, {
671
+ iconName: iconName,
672
+ modes: modes
673
+ }),
674
+ modes: modes,
675
+ onPress: onPress,
676
+ accessibilityLabel: label,
677
+ accessibilityHint: accessibilityHint,
678
+ accessibilityState: accessibilityState,
679
+ webAccessibilityProps: webAccessibilityProps
680
+ });
681
+ }
682
+
349
683
  // Attach Bento as a property of Section using namespace pattern
350
684
  Section.Bento = SectionBento;
351
685
  export default Section;