jfs-components 0.0.64 → 0.0.66

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 (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commonjs/components/CardCTA/CardCTA.js +15 -1
  3. package/lib/commonjs/components/Carousel/Carousel.js +34 -13
  4. package/lib/commonjs/components/Drawer/Drawer.js +9 -3
  5. package/lib/commonjs/components/IconButton/IconButton.js +42 -6
  6. package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
  7. package/lib/commonjs/components/Popup/Popup.js +2 -2
  8. package/lib/commonjs/components/Section/Section.js +22 -7
  9. package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
  10. package/lib/commonjs/icons/Icon.js +72 -75
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/commonjs/utils/MediaSource.js +181 -0
  13. package/lib/commonjs/utils/index.js +9 -1
  14. package/lib/module/components/CardCTA/CardCTA.js +15 -1
  15. package/lib/module/components/Carousel/Carousel.js +34 -13
  16. package/lib/module/components/Drawer/Drawer.js +9 -3
  17. package/lib/module/components/IconButton/IconButton.js +42 -6
  18. package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
  19. package/lib/module/components/Popup/Popup.js +2 -2
  20. package/lib/module/components/Section/Section.js +23 -8
  21. package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
  22. package/lib/module/icons/Icon.js +72 -75
  23. package/lib/module/icons/registry.js +1 -1
  24. package/lib/module/utils/MediaSource.js +176 -0
  25. package/lib/module/utils/index.js +2 -1
  26. package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
  27. package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
  28. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
  29. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
  30. package/lib/typescript/src/icons/Icon.d.ts +35 -16
  31. package/lib/typescript/src/icons/registry.d.ts +1 -1
  32. package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
  33. package/lib/typescript/src/utils/index.d.ts +2 -0
  34. package/package.json +1 -1
  35. package/src/components/CardCTA/CardCTA.tsx +13 -0
  36. package/src/components/Carousel/Carousel.tsx +37 -20
  37. package/src/components/Drawer/Drawer.tsx +13 -2
  38. package/src/components/IconButton/IconButton.tsx +70 -11
  39. package/src/components/IconCapsule/IconCapsule.tsx +13 -0
  40. package/src/components/Popup/Popup.tsx +2 -2
  41. package/src/components/Section/Section.tsx +29 -12
  42. package/src/components/UpiHandle/UpiHandle.tsx +37 -11
  43. package/src/icons/Icon.tsx +91 -76
  44. package/src/icons/registry.ts +1 -1
  45. package/src/utils/MediaSource.tsx +220 -0
  46. package/src/utils/index.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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
+
7
15
  ## [0.0.64] - 2026-04-20
8
16
 
9
17
  ### Added
@@ -61,8 +61,17 @@ function CardCTA({
61
61
  flexDirection: 'row',
62
62
  overflow: 'hidden'
63
63
  };
64
+
65
+ // NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
66
+ // Without them, Yoga's default `min-width: auto` clamps leftWrap to its
67
+ // single-line intrinsic text width, which steals all space from rightWrap
68
+ // and pushes the IconCapsule outside the card. See: text-not-wrapping
69
+ // inside flex rows on RN.
64
70
  const leftWrapStyle = {
65
71
  flex: 3,
72
+ flexShrink: 1,
73
+ flexBasis: 0,
74
+ minWidth: 0,
66
75
  paddingHorizontal: leftPaddingH,
67
76
  paddingVertical: leftPaddingV,
68
77
  gap: leftGap,
@@ -71,13 +80,18 @@ function CardCTA({
71
80
  };
72
81
  const rightWrapStyle = {
73
82
  flex: 2,
83
+ flexShrink: 1,
84
+ flexBasis: 0,
85
+ minWidth: 0,
74
86
  paddingHorizontal: rightPaddingH,
75
87
  paddingVertical: rightPaddingV,
76
88
  alignItems: 'flex-end',
77
89
  justifyContent: 'flex-start'
78
90
  };
79
91
  const textWrapStyle = {
80
- gap: textGap
92
+ gap: textGap,
93
+ alignSelf: 'stretch',
94
+ minWidth: 0
81
95
  };
82
96
  const titleStyle = {
83
97
  color: titleColor,
@@ -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);
@@ -188,8 +189,26 @@ function Carousel({
188
189
  onScrollBeginDrag: handleScrollBeginDrag,
189
190
  onScrollEndDrag: handleScrollEndDrag,
190
191
  children: items.map((child, index) => {
191
- const itemStyle = {
192
- width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined
192
+ // Strict slot box: width must be honored; never grow or shrink with
193
+ // content, and clip anything that misbehaves (e.g. a child whose
194
+ // inner flex layout would otherwise leak into the next slot on
195
+ // native).
196
+ const slotStyle = {
197
+ width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
198
+ flexGrow: 0,
199
+ flexShrink: 0,
200
+ flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
201
+ overflow: 'hidden'
202
+ };
203
+
204
+ // The cloned style forces the child's outer node to also honor the
205
+ // slot width strictly. Without this, a child with a weird intrinsic
206
+ // size can render wider than the slot and visually overflow.
207
+ const childOverrideStyle = {
208
+ width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
209
+ maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
210
+ flexGrow: 0,
211
+ flexShrink: 0
193
212
  };
194
213
 
195
214
  // Pass modes down to children
@@ -198,17 +217,17 @@ function Carousel({
198
217
  ...(child.props?.modes || {}),
199
218
  ...modes
200
219
  },
201
- style: [itemStyle, child.props?.style]
220
+ style: [childOverrideStyle, child.props?.style]
202
221
  }) : child;
203
222
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
204
- style: itemStyle,
223
+ style: slotStyle,
205
224
  children: childWithModes
206
225
  }, index);
207
226
  })
208
227
  }), showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
209
228
  modes: modes,
210
229
  style: {
211
- marginTop: paginationGap
230
+ marginTop: paginationOffset
212
231
  }
213
232
  })]
214
233
  })
@@ -250,13 +269,15 @@ function Pagination({
250
269
  } = (0, _react.useContext)(CarouselContext);
251
270
  const modes = propModes || ctxModes || {};
252
271
 
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');
272
+ // Token resolution for dots — matches Figma tokens
273
+ // (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
274
+ // Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
275
+ const dotSize = 6;
276
+ const dotActiveWidth = 16;
277
+ const dotGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/gap', modes) || '4');
278
+ const dotColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
279
+ const dotActiveColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
280
+ const dotRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/radius', modes) || '9999');
260
281
  const containerStyle = {
261
282
  flexDirection: 'row',
262
283
  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,8 +130,13 @@ 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
- }, []);
133
+ setMode(prev => {
134
+ if (prev !== newMode) {
135
+ onStateChange?.(newMode);
136
+ }
137
+ return newMode;
138
+ });
139
+ }, [onStateChange]);
134
140
 
135
141
  // Gesture policy:
136
142
  // • activeOffsetY: require a clear *vertical* drag (10px) before this
@@ -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"
@@ -90,7 +90,14 @@ const SLOT_GRID_MAX_COLUMNS = 4;
90
90
  const SLOT_GRID_STAGGER_CAP = 8;
91
91
  const SLOT_GRID_ENTER_STAGGER_MS = 35;
92
92
  const SLOT_GRID_EXIT_STAGGER_MS = 20;
93
+ const SLOT_GRID_ENTER_DURATION_MS = 220;
93
94
  const SLOT_GRID_EXIT_DURATION_MS = 160;
95
+ const SLOT_GRID_HEIGHT_DURATION_MS = 280;
96
+
97
+ // Standard ease-out cubic curve. Calm, professional, no overshoot — matches
98
+ // system-style transitions. Defined once at module scope so it isn't
99
+ // re-allocated per render.
100
+ const SLOT_GRID_EASING = _reactNativeReanimated.Easing.out(_reactNativeReanimated.Easing.cubic);
94
101
  const slotGridRowFlowStyle = {
95
102
  flexDirection: 'row',
96
103
  justifyContent: 'space-between'
@@ -136,6 +143,13 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
136
143
  const containerStyle = (0, _react.useMemo)(() => ({
137
144
  gap
138
145
  }), [gap]);
146
+ // Strict `width` (not `minWidth`) so every cell in every row is exactly the
147
+ // same size — `space-between` then distributes identical leftover into
148
+ // identical inter-cell gaps on every row, which keeps column N of row 1
149
+ // aligned with column N of rows 2/3/etc. Cells whose label is wider than
150
+ // `cellWidth` simply wrap their text onto more lines (taking more vertical
151
+ // space; the row's height grows naturally to fit the tallest cell, and the
152
+ // animated-height clip springs to the new total).
139
153
  const cellStyle = (0, _react.useMemo)(() => cellWidth !== null ? {
140
154
  width: cellWidth
141
155
  } : undefined, [cellWidth]);
@@ -169,8 +183,9 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
169
183
  // and an explicit `height` driven by a shared value.
170
184
  // 3. The inner view reports its natural height via `onLayout`. The first
171
185
  // 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.
186
+ // subsequent change (e.g. expand/collapse adds or removes rows) eases
187
+ // the shared value to the new natural height with a calm ease-out
188
+ // timing curve — no spring, no bounce, no overshoot.
174
189
  //
175
190
  // Visually: the container reveals/conceals content like a curtain, and the
176
191
  // cells never deform.
@@ -184,9 +199,9 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
184
199
  animatedHeight.value = h;
185
200
  return;
186
201
  }
187
- animatedHeight.value = (0, _reactNativeReanimated.withSpring)(h, {
188
- damping: 22,
189
- stiffness: 180,
202
+ animatedHeight.value = (0, _reactNativeReanimated.withTiming)(h, {
203
+ duration: SLOT_GRID_HEIGHT_DURATION_MS,
204
+ easing: SLOT_GRID_EASING,
190
205
  reduceMotion: _reactNativeReanimated.ReduceMotion.System
191
206
  });
192
207
  }, [animatedHeight]);
@@ -210,8 +225,8 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
210
225
  const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
211
226
  const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
212
227
  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);
228
+ const entering = _reactNativeReanimated.FadeInUp.duration(SLOT_GRID_ENTER_DURATION_MS).easing(SLOT_GRID_EASING).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
229
+ const exiting = _reactNativeReanimated.FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).easing(SLOT_GRID_EASING).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
215
230
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
216
231
  entering: entering,
217
232
  exiting: exiting,
@@ -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,
@@ -8,99 +8,96 @@ var _react = _interopRequireDefault(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
10
10
  var _registry = require("./registry");
11
+ var _MediaSource = _interopRequireDefault(require("../utils/MediaSource"));
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  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); }
13
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
15
  /**
15
- * Generic Icon Component
16
- *
17
- * Renders an icon from the registry by name with customizable size and color.
18
- *
19
- * @component
20
- * @param {Object} props - Component props
21
- * @param {string} props.name - Icon name from the registry (e.g., 'ic_ccv', 'ic_card')
22
- * @param {number} [props.size=24] - Icon size in pixels (width and height)
23
- * @param {string} [props.color='#141414'] - Icon color (hex, rgb, or named color)
24
- * @param {Object} [props.style] - Additional styles for the container View
25
- *
16
+ * Generic Icon component.
17
+ *
18
+ * Renders an icon from the registry by `name`, or falls back to a
19
+ * smart-detected `source` (SVG / PNG / JPG / require / SVG component /
20
+ * remote URI). External sources are tinted with `color` so they participate
21
+ * in the design-token modes just like built-in icons.
22
+ *
26
23
  * @example
27
- * ```jsx
24
+ * ```tsx
25
+ * // Built-in icon from the registry.
28
26
  * <Icon name="ic_ccv" size={24} color="#141414" />
29
- * <Icon name="ic_card" size={32} color="#5c00b5" />
30
- * <Icon name="ic_cart" size={20} color="red" />
27
+ *
28
+ * // Fallback to a remote SVG (auto-detected by the .svg extension).
29
+ * <Icon source="https://cdn.example.com/avatar.svg" size={24} color="#5c00b5" />
30
+ *
31
+ * // Fallback to a local raster asset.
32
+ * <Icon source={require('./brand.png')} size={32} />
33
+ *
34
+ * // Fallback to an SVG React component (e.g. via react-native-svg-transformer).
35
+ * import BrandLogo from './brand.svg';
36
+ * <Icon source={BrandLogo} size={24} color="red" />
31
37
  * ```
32
38
  */
33
39
  function Icon({
34
40
  name,
41
+ source,
35
42
  size = 24,
36
43
  color = '#141414',
37
44
  style,
38
45
  ...rest
39
46
  }) {
40
- // Validate icon name
41
- if (!name) {
42
- console.warn('Icon: name prop is required');
43
- return null;
44
- }
45
- if (!(0, _registry.hasIcon)(name)) {
46
- const {
47
- getIconNames
48
- } = require('./registry');
49
- console.warn(`Icon: "${name}" not found in registry. Available icons: ${getIconNames().join(', ')}`);
50
- return null;
51
- }
52
-
53
- // Get icon data from registry
54
- const iconData = (0, _registry.getIcon)(name);
55
- if (!iconData) {
56
- return null;
57
- }
58
-
59
- // Parse viewBox to get width and height for aspect ratio
60
- const viewBoxParts = iconData.viewBox.split(' ');
61
- // @ts-ignore
62
- const viewBoxWidth = parseFloat(viewBoxParts[2]) || size;
63
- // @ts-ignore
64
- const viewBoxHeight = parseFloat(viewBoxParts[3]) || size;
65
-
66
- // Calculate aspect ratio to maintain proper scaling
67
- const aspectRatio = viewBoxWidth / viewBoxHeight;
68
-
69
- // Determine actual width and height based on size and aspect ratio
70
- let width = size;
71
- let height = size;
72
-
73
- // If viewBox is not square, adjust dimensions to maintain aspect ratio
74
- if (Math.abs(aspectRatio - 1) > 0.01) {
75
- if (aspectRatio > 1) {
76
- // Wider than tall
77
- height = size / aspectRatio;
78
- } else {
79
- // Taller than wide
80
- width = size * aspectRatio;
81
- }
82
- }
83
- const containerStyle = {
47
+ const containerStyle = [{
84
48
  width: size,
85
49
  height: size,
86
50
  alignItems: 'center',
87
- justifyContent: 'center',
88
- ...style
89
- };
90
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
91
- style: containerStyle,
92
- ...rest,
93
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.default, {
94
- width: width,
95
- height: height,
96
- viewBox: iconData.viewBox,
97
- preserveAspectRatio: "xMidYMid meet",
98
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
99
- d: iconData.path,
100
- fill: color,
101
- fillRule: iconData.fillRule || 'nonzero'
51
+ justifyContent: 'center'
52
+ }, style];
53
+ const iconData = name && (0, _registry.hasIcon)(name) ? (0, _registry.getIcon)(name) : null;
54
+ if (iconData) {
55
+ const viewBoxParts = iconData.viewBox.split(' ');
56
+ const viewBoxWidth = parseFloat(viewBoxParts[2] ?? `${size}`) || size;
57
+ const viewBoxHeight = parseFloat(viewBoxParts[3] ?? `${size}`) || size;
58
+ const aspectRatio = viewBoxWidth / viewBoxHeight;
59
+ let width = size;
60
+ let height = size;
61
+ if (Math.abs(aspectRatio - 1) > 0.01) {
62
+ if (aspectRatio > 1) {
63
+ height = size / aspectRatio;
64
+ } else {
65
+ width = size * aspectRatio;
66
+ }
67
+ }
68
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
69
+ style: containerStyle,
70
+ ...rest,
71
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.default, {
72
+ width: width,
73
+ height: height,
74
+ viewBox: iconData.viewBox,
75
+ preserveAspectRatio: "xMidYMid meet",
76
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
77
+ d: iconData.path,
78
+ fill: color,
79
+ fillRule: iconData.fillRule || 'nonzero'
80
+ })
81
+ })
82
+ });
83
+ }
84
+ if (source !== undefined) {
85
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
86
+ style: containerStyle,
87
+ ...rest,
88
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MediaSource.default, {
89
+ source: source,
90
+ size: size,
91
+ tintColor: color,
92
+ resizeMode: "contain"
102
93
  })
103
- })
104
- });
94
+ });
95
+ }
96
+ if (!name) {
97
+ console.warn('Icon: either `name` or `source` is required');
98
+ return null;
99
+ }
100
+ console.warn(`Icon: "${name}" not found in registry and no \`source\` fallback was provided.`);
101
+ return null;
105
102
  }
106
103
  var _default = exports.default = Icon;