jfs-components 0.0.63 → 0.0.65

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 (44) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/lib/commonjs/components/Carousel/Carousel.js +12 -9
  3. package/lib/commonjs/components/Drawer/Drawer.js +116 -50
  4. package/lib/commonjs/components/IconButton/IconButton.js +42 -6
  5. package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
  6. package/lib/commonjs/components/Popup/Popup.js +2 -2
  7. package/lib/commonjs/components/Section/Section.js +280 -58
  8. package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
  9. package/lib/commonjs/icons/Icon.js +72 -75
  10. package/lib/commonjs/icons/registry.js +1 -1
  11. package/lib/commonjs/utils/MediaSource.js +181 -0
  12. package/lib/commonjs/utils/index.js +9 -1
  13. package/lib/module/components/Carousel/Carousel.js +12 -9
  14. package/lib/module/components/Drawer/Drawer.js +116 -50
  15. package/lib/module/components/IconButton/IconButton.js +42 -6
  16. package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
  17. package/lib/module/components/Popup/Popup.js +2 -2
  18. package/lib/module/components/Section/Section.js +280 -58
  19. package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
  20. package/lib/module/icons/Icon.js +72 -75
  21. package/lib/module/icons/registry.js +1 -1
  22. package/lib/module/utils/MediaSource.js +176 -0
  23. package/lib/module/utils/index.js +2 -1
  24. package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
  25. package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
  26. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
  27. package/lib/typescript/src/components/Section/Section.d.ts +42 -1
  28. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
  29. package/lib/typescript/src/icons/Icon.d.ts +35 -16
  30. package/lib/typescript/src/icons/registry.d.ts +1 -1
  31. package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
  32. package/lib/typescript/src/utils/index.d.ts +2 -0
  33. package/package.json +1 -1
  34. package/src/components/Carousel/Carousel.tsx +16 -17
  35. package/src/components/Drawer/Drawer.tsx +136 -60
  36. package/src/components/IconButton/IconButton.tsx +70 -11
  37. package/src/components/IconCapsule/IconCapsule.tsx +13 -0
  38. package/src/components/Popup/Popup.tsx +2 -2
  39. package/src/components/Section/Section.tsx +411 -71
  40. package/src/components/UpiHandle/UpiHandle.tsx +37 -11
  41. package/src/icons/Icon.tsx +91 -76
  42. package/src/icons/registry.ts +1 -1
  43. package/src/utils/MediaSource.tsx +220 -0
  44. package/src/utils/index.ts +2 -0
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
+ var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
9
10
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
11
  var _NavArrow = _interopRequireDefault(require("../NavArrow/NavArrow"));
12
+ var _IconCapsule = _interopRequireDefault(require("../IconCapsule/IconCapsule"));
13
+ var _ListItem = _interopRequireDefault(require("../ListItem/ListItem"));
11
14
  var _webPlatformUtils = require("../../utils/web-platform-utils");
12
15
  var _reactUtils = require("../../utils/react-utils");
13
16
  var _jsxRuntime = require("react/jsx-runtime");
@@ -37,76 +40,95 @@ const headerFocusStyle = {
37
40
  };
38
41
 
39
42
  // ---------------------------------------------------------------------------
40
- // Shared grid layout: measures the widest child once per item-count, then
41
- // renders uniform-width cells laid out with `justify-content: space-between`.
42
- // This preserves three visual invariants regardless of viewport width:
43
+ // Shared grid layout first-row-anchored sizing.
44
+ //
45
+ // We measure each cell of the *first row* via `onLayout`, take their max as
46
+ // the canonical cellWidth, then apply that explicit width to every cell in
47
+ // every row. Combined with `justify-content: space-between`, this preserves
48
+ // three visual invariants regardless of viewport width:
43
49
  // 1. The first cell hugs the container's left edge.
44
50
  // 2. The last cell hugs the container's right edge.
45
51
  // 3. Cells in row N column K align with cells in row N+1 column K
46
52
  // (uniform cell width across the whole grid).
47
- // Pure flex sizing (e.g. `flexBasis: 0; flexGrow: 1`) cannot satisfy (1) and
48
- // (2) on wide viewports — it distributes extra space inside each cell, which
49
- // makes the icon+label content drift toward each cell's center and produces
50
- // visible "dead" margins on the left and right of the grid.
51
53
  //
52
- // To avoid the blank-flash that the previous implementation suffered from
53
- // (it hid the grid with `opacity: 0` until measurement completed, and reset
54
- // every measurement when the item count changed):
55
- // * Cells render at their *natural* widths during measurement instead of
56
- // being hidden. With `space-between`, the first/last cells already hug
57
- // the edges; only the column alignment can be off by a few pixels for
58
- // the single frame between mount and the onLayout callback.
59
- // * On item-count change (e.g. expand / collapse), we clear the per-cell
60
- // samples but keep the rendered layout visible — we never blank out.
61
- // * No 500ms safety timeout is needed because the grid is visible from the
62
- // first frame.
54
+ // Why first-row-anchored?
55
+ //
56
+ // The first row is *always present* (collapsed and expanded both render it),
57
+ // so the measurement happens exactly once on first mount and stays valid for
58
+ // the lifetime of the SlotGrid the cellWidth is *stable across toggles*.
59
+ // New cells in row 2+ mount when the user expands, and by that time
60
+ // `cellWidth` is already cached, so their `Animated.View` `entering` cascade
61
+ // is never interrupted by a re-measurement-driven re-render.
62
+ //
63
+ // Why not container-width math (e.g. `(containerWidth - gaps) / columns`)?
64
+ // That makes every cell wide enough to fill its share of the row. On wide
65
+ // viewports each cell becomes much wider than its content (icon + label),
66
+ // the content centers inside its oversized cell, and the visible result is
67
+ // a "dead margin" of empty space on the left and right of the grid. Sizing
68
+ // cells to natural content + `space-between` instead pushes the first cell
69
+ // flush left and the last cell flush right, distributing leftover space as
70
+ // the inter-cell gap.
71
+ //
72
+ // Why not measure every cell?
73
+ // The original implementation did, and `max()` could change when the item
74
+ // count changed (collapsed picks one max, expanded another), producing a
75
+ // visible width jump on toggle. The per-cell remeasurement also forced a
76
+ // re-render in the same React batch as the `entering` animation mounting,
77
+ // which Reanimated treats as a cancellation signal — cells visibly didn't
78
+ // animate. Anchoring to the first row eliminates both costs.
79
+ //
80
+ // First-frame behavior is preserved (no blank-flash): until the first-row
81
+ // `onLayout` fires, cells render at their natural width with `space-between`
82
+ // already laying them out correctly; the only thing that changes after
83
+ // measurement is that subsequent rows snap to the same cellWidth so columns
84
+ // align.
63
85
  // ---------------------------------------------------------------------------
64
86
  const SLOT_GRID_MAX_COLUMNS = 4;
65
- const slotGridRowStyle = {
87
+
88
+ // Beyond this many "extra" cells, additional cells reuse the cap delay so very
89
+ // large grids (16, 32, …) still feel snappy instead of cascading for >1s.
90
+ const SLOT_GRID_STAGGER_CAP = 8;
91
+ const SLOT_GRID_ENTER_STAGGER_MS = 35;
92
+ const SLOT_GRID_EXIT_STAGGER_MS = 20;
93
+ const SLOT_GRID_EXIT_DURATION_MS = 160;
94
+ const slotGridRowFlowStyle = {
66
95
  flexDirection: 'row',
67
96
  justifyContent: 'space-between'
68
97
  };
69
98
  const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
70
99
  items,
71
100
  gap,
72
- maxColumns = SLOT_GRID_MAX_COLUMNS
101
+ maxColumns = SLOT_GRID_MAX_COLUMNS,
102
+ animateExtrasFromIndex,
103
+ animateContainerLayout
73
104
  }) {
74
105
  const totalItems = items.length;
75
- const [maxItemWidth, setMaxItemWidth] = (0, _react.useState)(null);
76
- // Tracks the item-count that `maxItemWidth` corresponds to. When the
77
- // current `totalItems` differs, the existing measurement is considered
78
- // stale and cells fall back to natural widths until remeasurement.
79
- const [measuredForCount, setMeasuredForCount] = (0, _react.useState)(0);
80
- const itemWidthsRef = (0, _react.useRef)(new Map());
106
+ const columns = Math.min(maxColumns, totalItems || 1);
107
+ // Number of cells in the first row. Capped by `columns` (a fully-filled row)
108
+ // and by `totalItems` (e.g. a 1-item grid has a 1-cell first row).
109
+ const firstRowSize = Math.min(columns, totalItems);
81
110
 
82
- // Synchronously invalidate per-cell samples when the item count changes
83
- // (e.g. show more / less). We deliberately do NOT touch `maxItemWidth` or
84
- // `measuredForCount` state here flipping them would force an extra render
85
- // pass; instead we let the count mismatch (`measuredForCount !== totalItems`)
86
- // gate the use of the stale value, and the next onLayout cycle will publish
87
- // a fresh `maxItemWidth` for the new count.
88
- const prevTotalRef = (0, _react.useRef)(totalItems);
89
- if (prevTotalRef.current !== totalItems) {
90
- prevTotalRef.current = totalItems;
91
- itemWidthsRef.current = new Map();
92
- }
93
- const handleItemLayout = (0, _react.useCallback)((index, width) => {
94
- const widths = itemWidthsRef.current;
111
+ // First-row width measurement. Only cells whose `itemIndex < firstRowSize`
112
+ // get an `onLayout` callback. Once we have a width sample for each first-row
113
+ // cell, we publish the max and the callbacks become inert — no further
114
+ // measurement happens for the rest of the SlotGrid's lifetime, so toggles
115
+ // never trigger a re-measurement-driven re-render.
116
+ const [firstRowMaxWidth, setFirstRowMaxWidth] = (0, _react.useState)(null);
117
+ const firstRowWidthsRef = (0, _react.useRef)(new Map());
118
+ const handleFirstRowItemLayout = (0, _react.useCallback)((index, width) => {
119
+ const widths = firstRowWidthsRef.current;
95
120
  const previous = widths.get(index);
96
121
  if (previous !== undefined && Math.abs(previous - width) < 0.5) return;
97
122
  widths.set(index, width);
98
- if (widths.size >= totalItems && totalItems > 0) {
123
+ if (widths.size >= firstRowSize && firstRowSize > 0) {
99
124
  let newMax = 0;
100
125
  for (const w of widths.values()) {
101
126
  if (w > newMax) newMax = w;
102
127
  }
103
- setMaxItemWidth(prev => prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax);
104
- setMeasuredForCount(totalItems);
128
+ setFirstRowMaxWidth(prev => prev !== null && Math.abs(prev - newMax) < 0.5 ? prev : newMax);
105
129
  }
106
- }, [totalItems]);
107
- const hasFreshMeasurement = maxItemWidth !== null && measuredForCount === totalItems;
108
- const cellWidth = hasFreshMeasurement ? maxItemWidth : undefined;
109
- const columns = Math.min(maxColumns, totalItems || 1);
130
+ }, [firstRowSize]);
131
+ const cellWidth = firstRowMaxWidth;
110
132
  const rows = [];
111
133
  for (let i = 0; i < items.length; i += columns) {
112
134
  rows.push(items.slice(i, i + columns));
@@ -114,34 +136,131 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
114
136
  const containerStyle = (0, _react.useMemo)(() => ({
115
137
  gap
116
138
  }), [gap]);
117
- const measuredCellStyle = (0, _react.useMemo)(() => cellWidth !== undefined ? {
139
+ const cellStyle = (0, _react.useMemo)(() => cellWidth !== null ? {
118
140
  width: cellWidth
119
141
  } : undefined, [cellWidth]);
120
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
121
- style: containerStyle,
142
+
143
+ // `space-between` distributes any leftover row space as inter-cell gap, so
144
+ // the first cell is flush-left and the last cell is flush-right regardless
145
+ // of whether cellWidth has been measured yet. Identical layout strategy
146
+ // before and after measurement — no first-frame layout shift.
147
+ const rowStyle = slotGridRowFlowStyle;
148
+ const animationsEnabled = animateExtrasFromIndex !== undefined;
149
+ // Resolve the threshold once. When undefined we treat it as
150
+ // Number.POSITIVE_INFINITY so the per-cell branch always picks the plain
151
+ // `<View>` path.
152
+ const extrasThreshold = animationsEnabled ? animateExtrasFromIndex : Number.POSITIVE_INFINITY;
153
+ // Total count of "extra" cells currently rendered. Used to compute the
154
+ // reverse-stagger delay for the exiting animation so that the last cell
155
+ // leaves first.
156
+ const extrasCount = animationsEnabled ? Math.max(0, totalItems - extrasThreshold) : 0;
157
+ const useAnimatedContainer = animateContainerLayout === true;
158
+
159
+ // Explicit-height clip animation:
160
+ //
161
+ // Reanimated's `LinearTransition` interpolates the container's bounds, and
162
+ // in practice that interpolation drags on the cells inside (they momentarily
163
+ // appear squashed because the parent is shorter than its natural content
164
+ // for the duration of the animation). To keep cells at their *natural size
165
+ // throughout*, we instead:
166
+ // 1. Render the rows inside an inner `<View>` that sizes to its content
167
+ // naturally — cells are never squeezed, never resized.
168
+ // 2. Wrap that inner view in an `Animated.View` with `overflow: 'hidden'`
169
+ // and an explicit `height` driven by a shared value.
170
+ // 3. The inner view reports its natural height via `onLayout`. The first
171
+ // measurement snaps the shared value (no first-mount animation). Every
172
+ // subsequent change (e.g. expand/collapse adds or removes rows) springs
173
+ // the shared value to the new natural height.
174
+ //
175
+ // Visually: the container reveals/conceals content like a curtain, and the
176
+ // cells never deform.
177
+ const animatedHeight = (0, _reactNativeReanimated.useSharedValue)(-1);
178
+ const isFirstHeightLayoutRef = (0, _react.useRef)(true);
179
+ const handleContentLayout = (0, _react.useCallback)(e => {
180
+ const h = e.nativeEvent.layout.height;
181
+ if (h <= 0) return;
182
+ if (isFirstHeightLayoutRef.current) {
183
+ isFirstHeightLayoutRef.current = false;
184
+ animatedHeight.value = h;
185
+ return;
186
+ }
187
+ animatedHeight.value = (0, _reactNativeReanimated.withSpring)(h, {
188
+ damping: 22,
189
+ stiffness: 180,
190
+ reduceMotion: _reactNativeReanimated.ReduceMotion.System
191
+ });
192
+ }, [animatedHeight]);
193
+ const animatedHeightStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => animatedHeight.value < 0 ? {} : {
194
+ height: animatedHeight.value,
195
+ overflow: 'hidden'
196
+ });
197
+ const rowsChildren = /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
122
198
  children: rows.map((row, rowIndex) => {
123
199
  const spacersNeeded = row.length < columns ? columns - row.length : 0;
124
200
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
125
- style: slotGridRowStyle,
201
+ style: rowStyle,
126
202
  children: [row.map((child, colIndex) => {
127
203
  const itemIndex = rowIndex * columns + colIndex;
204
+ // Only first-row cells participate in measurement, and only
205
+ // until firstRowMaxWidth has been published. After that the
206
+ // onLayout becomes a no-op (we elide it).
207
+ const onLayoutHandler = firstRowMaxWidth === null && itemIndex < firstRowSize ? e => handleFirstRowItemLayout(itemIndex, e.nativeEvent.layout.width) : undefined;
208
+ if (itemIndex >= extrasThreshold) {
209
+ const extraOrdinal = itemIndex - extrasThreshold;
210
+ const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
211
+ const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
212
+ const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
213
+ const entering = _reactNativeReanimated.FadeInUp.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
214
+ const exiting = _reactNativeReanimated.FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
215
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
216
+ entering: entering,
217
+ exiting: exiting,
218
+ ...(onLayoutHandler ? {
219
+ onLayout: onLayoutHandler
220
+ } : null),
221
+ style: cellStyle,
222
+ children: child
223
+ }, itemIndex);
224
+ }
128
225
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
129
- onLayout: hasFreshMeasurement ? undefined : e => handleItemLayout(itemIndex, e.nativeEvent.layout.width),
130
- style: measuredCellStyle,
226
+ ...(onLayoutHandler ? {
227
+ onLayout: onLayoutHandler
228
+ } : null),
229
+ style: cellStyle,
131
230
  children: child
132
231
  }, itemIndex);
133
- }), hasFreshMeasurement && spacersNeeded > 0 && Array.from({
232
+ }), cellWidth !== null && spacersNeeded > 0 && Array.from({
134
233
  length: spacersNeeded
135
234
  }, (_, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
136
- style: measuredCellStyle
235
+ style: cellStyle
137
236
  }, `spacer-${i}`))]
138
237
  }, rowIndex);
139
238
  })
140
239
  });
240
+ if (useAnimatedContainer) {
241
+ // Outer Animated.View clips and animates height. Inner View holds the
242
+ // rows at natural size and reports its natural height for the spring
243
+ // target. Cell-width measurement happens on the cells themselves
244
+ // (first-row only) — no container-level onLayout is needed.
245
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
246
+ style: animatedHeightStyle,
247
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
248
+ style: containerStyle,
249
+ onLayout: handleContentLayout,
250
+ children: rowsChildren
251
+ })
252
+ });
253
+ }
254
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
255
+ style: containerStyle,
256
+ children: rowsChildren
257
+ });
141
258
  }, slotGridPropsAreEqual);
142
259
  function slotGridPropsAreEqual(prev, next) {
143
260
  if (prev.gap !== next.gap) return false;
144
261
  if ((prev.maxColumns ?? SLOT_GRID_MAX_COLUMNS) !== (next.maxColumns ?? SLOT_GRID_MAX_COLUMNS)) return false;
262
+ if (prev.animateExtrasFromIndex !== next.animateExtrasFromIndex) return false;
263
+ if (prev.animateContainerLayout !== next.animateContainerLayout) return false;
145
264
  if (prev.items === next.items) return true;
146
265
  if (prev.items.length !== next.items.length) return false;
147
266
  for (let i = 0; i < prev.items.length; i++) {
@@ -428,9 +547,17 @@ function SectionBento({
428
547
  // Same rationale as Section: accepted on the type but unused internally.
429
548
  accessibilityLabel: _accessibilityLabel,
430
549
  accessibilityHint,
550
+ collapsedCount = SLOT_GRID_MAX_COLUMNS,
551
+ defaultExpanded = false,
552
+ expanded: controlledExpanded,
553
+ onExpandedChange,
554
+ toggleMoreLabel = 'More',
555
+ toggleLessLabel = 'Less',
556
+ toggleMoreIcon = 'ic_chevron_down',
557
+ toggleLessIcon = 'ic_chevron_up',
558
+ renderToggle,
431
559
  ...rest
432
560
  }) {
433
- // Resolve section container tokens
434
561
  const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('section/background/color', modes) || '#ffffff';
435
562
  const gap = (0, _figmaVariablesResolver.getVariableByName)('section/gap', modes) || 12;
436
563
  const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('section/padding/horizontal', modes) || 12;
@@ -445,6 +572,51 @@ function SectionBento({
445
572
  }), [backgroundColor, paddingHorizontal, paddingVertical, radius, gap]);
446
573
  const processedNavSlot = (0, _react.useMemo)(() => navSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(navSlot), modes) : null, [navSlot, modes]);
447
574
  const processedUpiSlot = (0, _react.useMemo)(() => upiSlot ? (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(upiSlot), modes) : null, [upiSlot, modes]);
575
+
576
+ // `canExpand` is true iff there are strictly more real items than fit into
577
+ // the collapsed grid. When `allRealItems.length === collapsedCount` we just
578
+ // render them all with no toggle — identical to the pre-feature behavior.
579
+ const allRealItems = (0, _react.useMemo)(() => processedNavSlot ?? [], [processedNavSlot]);
580
+ const canExpand = allRealItems.length > collapsedCount;
581
+ const isControlled = controlledExpanded !== undefined;
582
+ const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
583
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
584
+
585
+ // Mirror onExpandedChange in a ref so `toggle` can stay referentially stable.
586
+ const onExpandedChangeRef = (0, _react.useRef)(onExpandedChange);
587
+ onExpandedChangeRef.current = onExpandedChange;
588
+ const isControlledRef = (0, _react.useRef)(isControlled);
589
+ isControlledRef.current = isControlled;
590
+ const expandedRef = (0, _react.useRef)(expanded);
591
+ expandedRef.current = expanded;
592
+ const toggle = (0, _react.useCallback)(() => {
593
+ const next = !expandedRef.current;
594
+ if (!isControlledRef.current) {
595
+ setInternalExpanded(next);
596
+ }
597
+ onExpandedChangeRef.current?.(next);
598
+ }, []);
599
+ const navGridItems = (0, _react.useMemo)(() => {
600
+ if (!canExpand) {
601
+ return allRealItems;
602
+ }
603
+ // Leave the last collapsed slot for the toggle cell.
604
+ const visibleRealItems = expanded ? allRealItems : allRealItems.slice(0, collapsedCount - 1);
605
+ const toggleNode = renderToggle ? renderToggle({
606
+ expanded,
607
+ toggle
608
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(DefaultBentoToggle, {
609
+ expanded: expanded,
610
+ onPress: toggle,
611
+ modes: modes,
612
+ moreLabel: toggleMoreLabel,
613
+ lessLabel: toggleLessLabel,
614
+ moreIcon: toggleMoreIcon,
615
+ lessIcon: toggleLessIcon,
616
+ extraCount: allRealItems.length - (collapsedCount - 1)
617
+ });
618
+ return [...visibleRealItems, toggleNode];
619
+ }, [canExpand, allRealItems, expanded, collapsedCount, renderToggle, toggle, modes, toggleMoreLabel, toggleLessLabel, toggleMoreIcon, toggleLessIcon]);
448
620
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
449
621
  style: [containerStyle, style],
450
622
  ...(_reactNative.Platform.OS === 'web' ? {
@@ -453,9 +625,13 @@ function SectionBento({
453
625
  accessibilityLabel: undefined,
454
626
  accessibilityHint: accessibilityHint,
455
627
  ...rest,
456
- children: [processedNavSlot && /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
457
- items: processedNavSlot,
458
- gap: gap
628
+ children: [navGridItems.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(SlotGrid, {
629
+ items: navGridItems,
630
+ gap: gap,
631
+ ...(canExpand ? {
632
+ animateExtrasFromIndex: collapsedCount,
633
+ animateContainerLayout: true
634
+ } : null)
459
635
  }), processedUpiSlot && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
460
636
  style: sectionBentoUpiRowStyle,
461
637
  children: processedUpiSlot
@@ -463,6 +639,52 @@ function SectionBento({
463
639
  });
464
640
  }
465
641
 
642
+ // ---------------------------------------------------------------------------
643
+ // DefaultBentoToggle — internal toggle cell rendered by `SectionBento` when no
644
+ // `renderToggle` prop is provided. Uses a vertical `ListItem` so the cell
645
+ // matches the visual rhythm of the surrounding nav items in every mode.
646
+ //
647
+ // Two icons (`ic_chevron_down` / `ic_chevron_up`) are used instead of a
648
+ // rotating single icon, because the toggle cell is reconciled by position
649
+ // (collapsed: end of row 1; expanded: end of last row), so any persistent
650
+ // shared-value-driven rotation would lose its anchor across toggles.
651
+ // ---------------------------------------------------------------------------
652
+
653
+ function DefaultBentoToggle({
654
+ expanded,
655
+ onPress,
656
+ modes,
657
+ moreLabel,
658
+ lessLabel,
659
+ moreIcon,
660
+ lessIcon,
661
+ extraCount
662
+ }) {
663
+ const label = expanded ? lessLabel : moreLabel;
664
+ const iconName = expanded ? lessIcon : moreIcon;
665
+ const accessibilityState = (0, _react.useMemo)(() => ({
666
+ expanded
667
+ }), [expanded]);
668
+ const webAccessibilityProps = (0, _react.useMemo)(() => ({
669
+ ariaExpanded: expanded
670
+ }), [expanded]);
671
+ const accessibilityHint = expanded ? `Hides ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}` : `Shows ${extraCount} additional ${extraCount === 1 ? 'action' : 'actions'}`;
672
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ListItem.default, {
673
+ layout: "Vertical",
674
+ supportText: label,
675
+ leading: /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
676
+ iconName: iconName,
677
+ modes: modes
678
+ }),
679
+ modes: modes,
680
+ onPress: onPress,
681
+ accessibilityLabel: label,
682
+ accessibilityHint: accessibilityHint,
683
+ accessibilityState: accessibilityState,
684
+ webAccessibilityProps: webAccessibilityProps
685
+ });
686
+ }
687
+
466
688
  // Attach Bento as a property of Section using namespace pattern
467
689
  Section.Bento = SectionBento;
468
690
  var _default = exports.default = Section;
@@ -9,12 +9,13 @@ var _reactNative = require("react-native");
9
9
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
10
  var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
11
11
  var _reactUtils = require("../../utils/react-utils");
12
+ var _MediaSource = _interopRequireDefault(require("../../utils/MediaSource"));
12
13
  var _Icon = _interopRequireDefault(require("../../icons/Icon"));
13
14
  var _jsxRuntime = require("react/jsx-runtime");
14
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
16
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
17
  // Default static asset from the component folder.
17
- // Consumers can override the image via the `avatarSource` prop if needed.
18
+ // Consumers can override the image via the `source` prop if needed.
18
19
  const DEFAULT_AVATAR_IMAGE = require('./Image.png');
19
20
  const IS_WEB = _reactNative.Platform.OS === 'web';
20
21
  const IS_IOS = _reactNative.Platform.OS === 'ios';
@@ -88,7 +89,8 @@ function resolveUpiHandleTokens(modes) {
88
89
  * @param {Object} [props.modes={}] - Modes object passed directly to `getVariableByName`.
89
90
  * @param {boolean} [props.showIcon=true] - Toggles the trailing icon visibility.
90
91
  * @param {string} [props.iconName='ic_scan_qr_code'] - Icon name from the actions set.
91
- * @param {ImageSourcePropType} [props.avatarSource] - Optional custom image source for the avatar.
92
+ * @param {UnifiedSource} [props.source] - Unified avatar source (URI, inline SVG XML, `require()` asset, SVG React component, or React element). Smart-detects raster vs SVG so the same prop works on iOS, Android and web.
93
+ * @param {ImageSourcePropType|UnifiedSource} [props.avatarSource] - Deprecated alias for `source`; kept for back-compat.
92
94
  * @param {Function} [props.onClick] - Click/tap handler. Works as an alias for `onPress`.
93
95
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
94
96
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
@@ -106,6 +108,7 @@ function UpiHandle({
106
108
  modes: propModes = _reactUtils.EMPTY_MODES,
107
109
  showIcon = true,
108
110
  iconName = 'ic_scan_qr_code',
111
+ source,
109
112
  avatarSource,
110
113
  onPress,
111
114
  onClick,
@@ -154,13 +157,22 @@ function UpiHandle({
154
157
  pressed
155
158
  }) => [tokens.containerStyle, pressed ? pressedOverlayStyle : null, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
156
159
  const staticContainerStyle = (0, _react.useMemo)(() => [tokens.containerStyle, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
160
+
161
+ // `source` wins; `avatarSource` is the legacy fallback. Both are accepted
162
+ // as a UnifiedSource (string / number / {uri} / component / element), and
163
+ // the legacy `ImageSourcePropType` shapes naturally fit that union too.
164
+ const resolvedAvatarSource = source ?? avatarSource ?? DEFAULT_AVATAR_IMAGE;
165
+ const avatarSize = tokens.avatarStyle.width ?? 23;
157
166
  const innerContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
158
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
159
- source: avatarSource || DEFAULT_AVATAR_IMAGE,
167
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
160
168
  style: tokens.avatarStyle,
161
- resizeMode: "cover",
162
- accessibilityElementsHidden: true,
163
- importantForAccessibility: "no"
169
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSource.default, {
170
+ source: resolvedAvatarSource,
171
+ size: avatarSize,
172
+ resizeMode: "cover",
173
+ accessibilityElementsHidden: true,
174
+ importantForAccessibility: "no"
175
+ })
164
176
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
165
177
  style: tokens.labelStyle,
166
178
  numberOfLines: 1,