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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.0.65] - 2026-04-21
8
+
9
+ ### Added
10
+
11
+ - **`Drawer` state callback:** Optional `onStateChange?: (state: 'collapsed' | 'expanded') => void` runs when the drawer settles into a new snap state (after gestures), so parents can react programmatically.
12
+
13
+ ---
14
+
15
+ ## [0.0.64] - 2026-04-20
16
+
17
+ ### Added
18
+
19
+ - **`Section.Bento` — expandable grid:** New built-in expand/collapse UX. Pass `collapsedCount` (default `4`) plus the full set of cells in `navSlot`, and `Section.Bento` auto-injects a toggle cell, owns the `expanded` boolean (uncontrolled), and animates extra cells in/out with a staggered fade cascade. Supports controlled mode via `expanded` + `onExpandedChange`, customizable labels (`toggleMoreLabel`, `toggleLessLabel`) and icons (`toggleMoreIcon`, `toggleLessIcon`), and a `renderToggle` escape hatch for non-`ListItem` toggles. The toggle is only injected when `navSlot.length > collapsedCount`, so existing usage at or below the threshold renders unchanged.
20
+ - **Drawer drop-shadow tokens:** New design tokens `drawer/shadow/primary/{offsetX,offsetY,blur,color}` and `drawer/shadow/secondary/{offsetX,color}` resolve a layered Figma drop shadow. Web stacks both shadows via `boxShadow`; iOS applies the primary shadow natively; Android uses elevation.
21
+
22
+ ### Changed
23
+
24
+ - **`SlotGrid` sizing — first-row-anchored:** `SlotGrid` (used by `Section.Bento`) now measures only the first row's cells once on mount and applies that max width to every cell in every row. Previously every cell was measured every render, which caused width jumps when the cell count changed (expand/collapse) and could cancel `Animated.View` `entering` cascades by re-rendering them in the same React batch as the mount. Result: stable cell width across toggles, no layout shift, animations play uninterrupted. Edge-flush behavior (first cell flush-left, last cell flush-right) is preserved via `justify-content: space-between`.
25
+ - **`Section.Bento` height transition:** The section grows and shrinks via an explicit measured-height spring (`overflow: 'hidden'` clip + `withSpring`) instead of `LinearTransition`. Cells inside are never resized during the animation — only the container's clip rectangle interpolates.
26
+ - **Drawer gesture policy:** Pan now requires a 10px vertical drag to activate (`activeOffsetY([-10, 10])`) and surrenders the gesture entirely after ~16px of horizontal movement (`failOffsetX([-16, 16])`). A defense-in-depth check in `onUpdate` skips Y translation on frames where horizontal motion dominates. This lets horizontal children (carousels, sliders, horizontal `FlatList`s) inside the drawer scroll cleanly without the sheet also translating.
27
+
28
+ ### Fixed
29
+
30
+ - **Drawer iOS shadow clipping:** The sheet now renders content inside an inner clip layer so `overflow: 'hidden'` no longer trims the outer view's drop shadow on iOS.
31
+
32
+ ### Accessibility
33
+
34
+ - All new `Section.Bento` animations honor the OS reduce-motion setting via Reanimated's `ReduceMotion.System`.
35
+
36
+ ---
37
+
7
38
  ## [0.0.63] - 2026-04-20
8
39
 
9
40
  ### Performance
@@ -51,7 +51,8 @@ function Carousel({
51
51
  const gap = gapProp ?? tokenGap;
52
52
  const containerPaddingH = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/horizontal', modes) || '0');
53
53
  const containerPaddingV = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/vertical', modes) || '0');
54
- const paginationGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/gap', modes) || '12');
54
+ // Spacing between the cards row and the pagination dots uses `carousel/gap`.
55
+ const paginationOffset = gap;
55
56
 
56
57
  // ---- Refs & state ----
57
58
  const scrollRef = (0, _react.useRef)(null);
@@ -208,7 +209,7 @@ function Carousel({
208
209
  }), showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
209
210
  modes: modes,
210
211
  style: {
211
- marginTop: paginationGap
212
+ marginTop: paginationOffset
212
213
  }
213
214
  })]
214
215
  })
@@ -250,13 +251,15 @@ function Pagination({
250
251
  } = (0, _react.useContext)(CarouselContext);
251
252
  const modes = propModes || ctxModes || {};
252
253
 
253
- // Token resolution for dots
254
- const dotSize = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotSize', modes) || '8');
255
- const dotActiveWidth = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotActiveWidth', modes) || '24');
256
- const dotGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotGap', modes) || '8');
257
- const dotColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotColor', modes) || 'rgba(255,255,255,0.35)';
258
- const dotActiveColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotActiveColor', modes) || '#ffffff';
259
- const dotRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/dotRadius', modes) || '4');
254
+ // Token resolution for dots — matches Figma tokens
255
+ // (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
256
+ // Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
257
+ const dotSize = 6;
258
+ const dotActiveWidth = 16;
259
+ const dotGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/gap', modes) || '4');
260
+ const dotColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
261
+ const dotActiveColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
262
+ const dotRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/radius', modes) || '9999');
260
263
  const containerStyle = {
261
264
  flexDirection: 'row',
262
265
  justifyContent: 'center',
@@ -64,7 +64,8 @@ function Drawer({
64
64
  accessibilityHint,
65
65
  contentContainerStyle,
66
66
  showsVerticalScrollIndicator = false,
67
- bottomInset = 80
67
+ bottomInset = 80,
68
+ onStateChange
68
69
  }) {
69
70
  const {
70
71
  height: screenHeight
@@ -129,9 +130,24 @@ function Drawer({
129
130
 
130
131
  // Update JS state for accessibility/logic if needed
131
132
  const updateMode = (0, _react.useCallback)(newMode => {
132
- setMode(newMode);
133
- }, []);
134
- const gesture = _reactNativeGestureHandler.Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-5, 5]).activeOffsetX([-5, 5]).onStart(() => {
133
+ setMode(prev => {
134
+ if (prev !== newMode) {
135
+ onStateChange?.(newMode);
136
+ }
137
+ return newMode;
138
+ });
139
+ }, [onStateChange]);
140
+
141
+ // Gesture policy:
142
+ // • activeOffsetY: require a clear *vertical* drag (10px) before this
143
+ // pan claims the gesture. Matches typical iOS scroll activation feel.
144
+ // • failOffsetX: if the finger crosses ~16px horizontally *before* we
145
+ // activate, surrender the gesture entirely so any horizontal child
146
+ // (FlatList horizontal, swiper, slider, etc.) can scroll cleanly
147
+ // without the drawer also translating on Y.
148
+ // • simultaneousWithExternalGesture(scrollRef): cooperate with the
149
+ // drawer's own internal vertical ScrollView for nested scrolling.
150
+ const gesture = _reactNativeGestureHandler.Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-10, 10]).failOffsetX([-16, 16]).onStart(() => {
135
151
  context.value = {
136
152
  y: translateY.value
137
153
  };
@@ -140,6 +156,16 @@ function Drawer({
140
156
  prevAtTop.value = scrollY.value <= 1;
141
157
  scrollTopTranslationOffset.value = 0;
142
158
  }).onUpdate(event => {
159
+ // Defense-in-depth: even after vertical activation, if the *current*
160
+ // motion is dominantly horizontal (e.g., the user activated with a
161
+ // small Y nudge and then curved into a horizontal swipe on a child
162
+ // carousel), don't translate the drawer this frame. failOffsetX
163
+ // already prevents activation in pure-horizontal swipes; this guards
164
+ // the diagonal-then-horizontal case.
165
+ if (Math.abs(event.translationX) > Math.abs(event.translationY) * 1.5) {
166
+ return;
167
+ }
168
+
143
169
  // Logic for nested scrolling:
144
170
  // If we are at the expanded position (minTranslateY) AND content is
145
171
  // scrolled down (scrollY > 0), let the ScrollView handle the gesture.
@@ -251,71 +277,108 @@ function Drawer({
251
277
  const titleWeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontWeight', modes) || '700';
252
278
  const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/lineHeight', modes) || 17;
253
279
  const titlePaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/bottom', modes) || 8;
280
+
281
+ // Drop shadow — Figma layers two shadows (primary + secondary) sharing
282
+ // the same offsetY/blur but with their own offsetX and color.
283
+ const shadowPrimaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetX', modes) ?? 0;
284
+ const shadowPrimaryOffsetY = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetY', modes) ?? 16;
285
+ const shadowPrimaryBlur = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/blur', modes) ?? 48;
286
+ const shadowPrimaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/color', modes) ?? 'rgba(12, 13, 16, 0.16)';
287
+ const shadowSecondaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/offsetX', modes) ?? 0;
288
+ const shadowSecondaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/color', modes) ?? 'rgba(12, 13, 16, 0.12)';
289
+
290
+ // Cross-platform shadow style. Web supports stacking two shadows via
291
+ // boxShadow. iOS only supports a single native shadow per view, so we
292
+ // apply the more prominent (primary) one. Android uses elevation.
293
+ const shadowStyle = _reactNative.Platform.select({
294
+ web: {
295
+ boxShadow: `${shadowSecondaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowSecondaryColor}, ` + `${shadowPrimaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowPrimaryColor}`
296
+ },
297
+ ios: {
298
+ shadowColor: shadowPrimaryColor,
299
+ shadowOffset: {
300
+ width: shadowPrimaryOffsetX,
301
+ height: shadowPrimaryOffsetY
302
+ },
303
+ shadowOpacity: 1,
304
+ shadowRadius: shadowPrimaryBlur / 2
305
+ },
306
+ android: {
307
+ elevation: 16
308
+ },
309
+ default: {}
310
+ });
254
311
  const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
255
312
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
256
313
  style: [styles.host, style],
257
314
  pointerEvents: "box-none",
258
315
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
259
316
  gesture: gesture,
260
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
317
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
261
318
  style: [styles.sheet, {
262
319
  // Constraint the height strictly to the expanded height
263
320
  // This ensures the ScrollView has a finite frame to scroll within
264
321
  height: expandedHeight,
265
322
  backgroundColor,
266
323
  borderTopLeftRadius: radius,
267
- borderTopRightRadius: radius,
268
- paddingLeft,
269
- paddingRight,
270
- paddingBottom,
271
- rowGap: drawerGap
272
- }, sheetStyle, animatedStyle],
324
+ borderTopRightRadius: radius
325
+ }, shadowStyle, sheetStyle, animatedStyle],
273
326
  accessible: true,
274
327
  ...(_reactNative.Platform.OS === 'web' ? {
275
328
  accessibilityRole: 'dialog'
276
329
  } : undefined),
277
330
  accessibilityLabel: undefined,
278
331
  accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
279
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
280
- style: [styles.handleArea, !title && !header && {
281
- paddingBottom: 0
332
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
333
+ style: [styles.sheetInner, {
334
+ borderTopLeftRadius: radius,
335
+ borderTopRightRadius: radius,
336
+ paddingLeft,
337
+ paddingRight,
338
+ paddingBottom,
339
+ rowGap: drawerGap
282
340
  }],
283
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
341
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
342
+ style: [styles.handleArea, !title && !header && {
343
+ paddingBottom: 0
344
+ }],
345
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
346
+ style: [{
347
+ backgroundColor: handleColor,
348
+ width: handleWidth,
349
+ height: handleHeight,
350
+ borderRadius: handleRadius
351
+ }]
352
+ })
353
+ }), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
284
354
  style: [{
285
- backgroundColor: handleColor,
286
- width: handleWidth,
287
- height: handleHeight,
288
- borderRadius: handleRadius
289
- }]
290
- })
291
- }), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
292
- style: [{
293
- color: titleColor,
294
- fontSize: titleSize,
295
- fontWeight: titleWeight,
296
- lineHeight: titleLineHeight,
297
- marginBottom: titlePaddingBottom
298
- }],
299
- children: title
300
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
301
- ref: scrollRef,
302
- style: [styles.content, contentStyle],
303
- contentContainerStyle: [{
304
- paddingBottom: paddingBottom + bottomInset,
305
- gap: drawerGap,
306
- flexDirection: 'column',
307
- alignItems: 'stretch'
308
- }, contentContainerStyle],
309
- showsVerticalScrollIndicator: showsVerticalScrollIndicator,
310
- animatedProps: animatedScrollProps,
311
- alwaysBounceVertical: false,
312
- overScrollMode: "always",
313
- onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
314
- scrollY.value = event.contentOffset.y;
315
- }),
316
- scrollEventThrottle: 16,
317
- children: children
318
- })]
355
+ color: titleColor,
356
+ fontSize: titleSize,
357
+ fontWeight: titleWeight,
358
+ lineHeight: titleLineHeight,
359
+ marginBottom: titlePaddingBottom
360
+ }],
361
+ children: title
362
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
363
+ ref: scrollRef,
364
+ style: [styles.content, contentStyle],
365
+ contentContainerStyle: [{
366
+ paddingBottom: paddingBottom + bottomInset,
367
+ gap: drawerGap,
368
+ flexDirection: 'column',
369
+ alignItems: 'stretch'
370
+ }, contentContainerStyle],
371
+ showsVerticalScrollIndicator: showsVerticalScrollIndicator,
372
+ animatedProps: animatedScrollProps,
373
+ alwaysBounceVertical: false,
374
+ overScrollMode: "always",
375
+ onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
376
+ scrollY.value = event.contentOffset.y;
377
+ }),
378
+ scrollEventThrottle: 16,
379
+ children: children
380
+ })]
381
+ })
319
382
  })
320
383
  })
321
384
  });
@@ -333,7 +396,10 @@ const styles = _reactNative.StyleSheet.create({
333
396
  sheet: {
334
397
  width: '100%',
335
398
  position: 'absolute',
336
- top: 0,
399
+ top: 0
400
+ },
401
+ sheetInner: {
402
+ flex: 1,
337
403
  overflow: 'hidden'
338
404
  },
339
405
  handleArea: {
@@ -78,8 +78,13 @@ function resolveIconButtonTokens(modes, disabled) {
78
78
  * pressed transform mirrored via React state) — removed.
79
79
  * - Wrapped in `React.memo`.
80
80
  */
81
+ // Legacy default icon used when neither a `name` nor a `source` is supplied
82
+ // for the resolved slot. Kept as a constant rather than a destructuring
83
+ // default so source-only call sites don't accidentally render `'ic_card'`.
84
+ const LEGACY_DEFAULT_ICON_NAME = 'ic_card';
81
85
  function IconButton({
82
- iconName = 'ic_card',
86
+ iconName,
87
+ source,
83
88
  modes = _reactUtils.EMPTY_MODES,
84
89
  onPress,
85
90
  disabled = false,
@@ -90,7 +95,9 @@ function IconButton({
90
95
  webAccessibilityProps,
91
96
  isToggle = false,
92
97
  activeIcon,
98
+ activeSource,
93
99
  inactiveIcon,
100
+ inactiveSource,
94
101
  isActive = false,
95
102
  ...rest
96
103
  }) {
@@ -113,11 +120,35 @@ function IconButton({
113
120
  userHandlersRef.current.onHoverIn = rest?.onHoverIn;
114
121
  userHandlersRef.current.onHoverOut = rest?.onHoverOut;
115
122
 
116
- // Determine which icon to display
117
- const finalIconName = isToggle ? isActive && activeIcon ? activeIcon : !isActive && inactiveIcon ? inactiveIcon : iconName : iconName;
123
+ // Resolve the active (name + source) pair for the current slot. Toggle
124
+ // mode picks active/inactive based on `isActive`; per-state overrides
125
+ // fall back to the default `iconName` / `source` when omitted. We then
126
+ // apply the legacy default icon only as a last resort, so a source-only
127
+ // call site (`<IconButton source="…" />`) renders the source instead of
128
+ // bleeding through to `'ic_card'`.
129
+ let resolvedIconName;
130
+ let resolvedSource;
131
+ if (isToggle) {
132
+ if (isActive) {
133
+ resolvedIconName = activeIcon ?? iconName;
134
+ resolvedSource = activeSource ?? source;
135
+ } else {
136
+ resolvedIconName = inactiveIcon ?? iconName;
137
+ resolvedSource = inactiveSource ?? source;
138
+ }
139
+ } else {
140
+ resolvedIconName = iconName;
141
+ resolvedSource = source;
142
+ }
143
+ if (!resolvedIconName && resolvedSource === undefined) {
144
+ resolvedIconName = LEGACY_DEFAULT_ICON_NAME;
145
+ }
118
146
 
119
- // Generate default accessibility label from icon name if not provided
120
- const defaultAccessibilityLabel = accessibilityLabel || iconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
147
+ // Generate default accessibility label from the resolved icon name when
148
+ // possible. Source-only call sites should provide an explicit
149
+ // `accessibilityLabel`; we fall back to a generic 'Icon button' so we
150
+ // never crash on `iconName.replace(...)` when only a `source` is supplied.
151
+ const defaultAccessibilityLabel = accessibilityLabel || (resolvedIconName ? resolvedIconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Icon button');
121
152
  const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
122
153
  restProps: rest,
123
154
  onPress: disabled ? undefined : onPress,
@@ -170,7 +201,12 @@ function IconButton({
170
201
  style: styleCallback,
171
202
  ...webProps,
172
203
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
173
- name: finalIconName,
204
+ ...(resolvedIconName !== undefined ? {
205
+ name: resolvedIconName
206
+ } : {}),
207
+ ...(resolvedSource !== undefined ? {
208
+ source: resolvedSource
209
+ } : {}),
174
210
  size: tokens.iconSize,
175
211
  color: tokens.iconColor,
176
212
  accessibilityElementsHidden: true,
@@ -49,6 +49,7 @@ function resolveIconCapsuleTokens(modes) {
49
49
  * @component
50
50
  * @param {Object} props - Component props
51
51
  * @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
52
+ * @param {UnifiedSource} [props.source] - Fallback source (remote URI, inline SVG XML, `require()` asset, SVG React component, or React element). Used when `iconName` is missing or unknown. Tinted with the mode-resolved icon color so it follows design tokens just like a built-in icon.
52
53
  * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
53
54
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
54
55
  * @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
@@ -62,6 +63,7 @@ function resolveIconCapsuleTokens(modes) {
62
63
  */
63
64
  function IconCapsule({
64
65
  iconName = 'ic_card',
66
+ source,
65
67
  modes: propModes = _reactUtils.EMPTY_MODES,
66
68
  // accessibilityLabel is accepted on the type for API back-compat but the
67
69
  // component intentionally renders `accessibilityLabel={undefined}` (icons
@@ -91,6 +93,9 @@ function IconCapsule({
91
93
  ...rest,
92
94
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
93
95
  name: iconName,
96
+ ...(source !== undefined ? {
97
+ source
98
+ } : {}),
94
99
  size: tokens.iconSize,
95
100
  color: tokens.iconColor,
96
101
  accessibilityElementsHidden: true,
@@ -112,12 +112,12 @@ const Popup = /*#__PURE__*/(0, _react.forwardRef)(function Popup({
112
112
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
113
113
  style: styles.overlay,
114
114
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
115
- style: [_reactNative.StyleSheet.absoluteFillObject, {
115
+ style: [_reactNative.StyleSheet.absoluteFill, {
116
116
  backgroundColor: backdropColor,
117
117
  opacity: backdropAnim
118
118
  }],
119
119
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
120
- style: _reactNative.StyleSheet.absoluteFillObject,
120
+ style: _reactNative.StyleSheet.absoluteFill,
121
121
  onPress: closeOnBackdropPress ? handleClose : undefined,
122
122
  accessibilityRole: "button",
123
123
  accessibilityLabel: "Close popup"