jfs-components 0.0.64 → 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 (40) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commonjs/components/Carousel/Carousel.js +12 -9
  3. package/lib/commonjs/components/Drawer/Drawer.js +9 -3
  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/UpiHandle/UpiHandle.js +19 -7
  8. package/lib/commonjs/icons/Icon.js +72 -75
  9. package/lib/commonjs/icons/registry.js +1 -1
  10. package/lib/commonjs/utils/MediaSource.js +181 -0
  11. package/lib/commonjs/utils/index.js +9 -1
  12. package/lib/module/components/Carousel/Carousel.js +12 -9
  13. package/lib/module/components/Drawer/Drawer.js +9 -3
  14. package/lib/module/components/IconButton/IconButton.js +42 -6
  15. package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
  16. package/lib/module/components/Popup/Popup.js +2 -2
  17. package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
  18. package/lib/module/icons/Icon.js +72 -75
  19. package/lib/module/icons/registry.js +1 -1
  20. package/lib/module/utils/MediaSource.js +176 -0
  21. package/lib/module/utils/index.js +2 -1
  22. package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
  23. package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
  24. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
  25. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
  26. package/lib/typescript/src/icons/Icon.d.ts +35 -16
  27. package/lib/typescript/src/icons/registry.d.ts +1 -1
  28. package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
  29. package/lib/typescript/src/utils/index.d.ts +2 -0
  30. package/package.json +1 -1
  31. package/src/components/Carousel/Carousel.tsx +16 -17
  32. package/src/components/Drawer/Drawer.tsx +13 -2
  33. package/src/components/IconButton/IconButton.tsx +70 -11
  34. package/src/components/IconCapsule/IconCapsule.tsx +13 -0
  35. package/src/components/Popup/Popup.tsx +2 -2
  36. package/src/components/UpiHandle/UpiHandle.tsx +37 -11
  37. package/src/icons/Icon.tsx +91 -76
  38. package/src/icons/registry.ts +1 -1
  39. package/src/utils/MediaSource.tsx +220 -0
  40. 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
@@ -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,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"
@@ -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;