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
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _reactNativeSvg = require("react-native-svg");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ /**
13
+ * A unified, "do-the-right-thing" image source accepted by `MediaSource` and
14
+ * by the `source` prop on `Icon`, `IconCapsule` and `UpiHandle`.
15
+ *
16
+ * Accepts any of:
17
+ * - `string` — a URI (raster or `.svg`) **or** an inline SVG XML document
18
+ * (`'<svg …>…</svg>'`).
19
+ * - `number` — a Metro asset id from `require('./foo.png')`.
20
+ * - `{ uri, … }` — the standard RN `ImageURISource` object (works for both
21
+ * raster and `.svg` URIs).
22
+ * - `React.ComponentType` — an SVG React component (e.g. produced by
23
+ * `react-native-svg-transformer`, by `@svgr/*`,
24
+ * or hand-written). It is rendered with
25
+ * `{ width, height, color, fill }` so it can be
26
+ * tinted just like a built-in icon.
27
+ * - `React.ReactElement` — an already-rendered node, passed through verbatim.
28
+ *
29
+ * The helper sniffs the input shape (no extension hint required from the
30
+ * caller) and picks the correct renderer for the platform.
31
+ */
32
+
33
+ const SVG_XML_RE = /<svg[\s>]/i;
34
+ const SVG_URI_RE = /\.svg(\?|#|$)/i;
35
+ function isSvgXml(s) {
36
+ return /^\s*</.test(s) && SVG_XML_RE.test(s);
37
+ }
38
+ function isSvgUri(s) {
39
+ return SVG_URI_RE.test(s);
40
+ }
41
+ function isUriObject(v) {
42
+ return typeof v === 'object' && v !== null && 'uri' in v && typeof v.uri === 'string';
43
+ }
44
+
45
+ /**
46
+ * Smart renderer that picks the right primitive for the source shape. See
47
+ * {@link UnifiedSource}.
48
+ *
49
+ * Designed to be used internally by `Icon`, `IconCapsule`, and `UpiHandle`,
50
+ * but also exported for ad-hoc consumer use.
51
+ */
52
+ function MediaSource(props) {
53
+ const {
54
+ source,
55
+ size,
56
+ width,
57
+ height,
58
+ tintColor,
59
+ style,
60
+ resizeMode = 'cover',
61
+ accessibilityElementsHidden,
62
+ importantForAccessibility
63
+ } = props;
64
+ const w = width ?? size;
65
+ const h = height ?? size;
66
+
67
+ // Pre-rendered element — pass through.
68
+ if (/*#__PURE__*/_react.default.isValidElement(source)) {
69
+ return source;
70
+ }
71
+
72
+ // SVG / icon component.
73
+ if (typeof source === 'function') {
74
+ const Comp = source;
75
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
76
+ ...(w !== undefined ? {
77
+ width: w
78
+ } : {}),
79
+ ...(h !== undefined ? {
80
+ height: h
81
+ } : {}),
82
+ ...(tintColor !== undefined ? {
83
+ color: tintColor,
84
+ fill: tintColor
85
+ } : {})
86
+ });
87
+ }
88
+ const sizeStyle = w !== undefined || h !== undefined ? {
89
+ width: w,
90
+ height: h
91
+ } : null;
92
+ const tintStyle = tintColor ? {
93
+ tintColor
94
+ } : null;
95
+ const composedStyle = [sizeStyle, tintStyle, style];
96
+ if (typeof source === 'string') {
97
+ if (isSvgXml(source)) {
98
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgXml, {
99
+ xml: source,
100
+ ...(w !== undefined ? {
101
+ width: w
102
+ } : {}),
103
+ ...(h !== undefined ? {
104
+ height: h
105
+ } : {}),
106
+ ...(tintColor !== undefined ? {
107
+ color: tintColor
108
+ } : {})
109
+ });
110
+ }
111
+ if (isSvgUri(source)) {
112
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgUri, {
113
+ uri: source,
114
+ ...(w !== undefined ? {
115
+ width: w
116
+ } : {}),
117
+ ...(h !== undefined ? {
118
+ height: h
119
+ } : {}),
120
+ ...(tintColor !== undefined ? {
121
+ color: tintColor
122
+ } : {})
123
+ });
124
+ }
125
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
126
+ source: {
127
+ uri: source
128
+ },
129
+ style: composedStyle,
130
+ resizeMode: resizeMode,
131
+ ...(accessibilityElementsHidden !== undefined ? {
132
+ accessibilityElementsHidden
133
+ } : {}),
134
+ ...(importantForAccessibility !== undefined ? {
135
+ importantForAccessibility
136
+ } : {})
137
+ });
138
+ }
139
+ if (isUriObject(source)) {
140
+ if (isSvgUri(source.uri)) {
141
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.SvgUri, {
142
+ uri: source.uri,
143
+ ...(w !== undefined ? {
144
+ width: w
145
+ } : {}),
146
+ ...(h !== undefined ? {
147
+ height: h
148
+ } : {}),
149
+ ...(tintColor !== undefined ? {
150
+ color: tintColor
151
+ } : {})
152
+ });
153
+ }
154
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
155
+ source: source,
156
+ style: composedStyle,
157
+ resizeMode: resizeMode,
158
+ ...(accessibilityElementsHidden !== undefined ? {
159
+ accessibilityElementsHidden
160
+ } : {}),
161
+ ...(importantForAccessibility !== undefined ? {
162
+ importantForAccessibility
163
+ } : {})
164
+ });
165
+ }
166
+ if (typeof source === 'number') {
167
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
168
+ source: source,
169
+ style: composedStyle,
170
+ resizeMode: resizeMode,
171
+ ...(accessibilityElementsHidden !== undefined ? {
172
+ accessibilityElementsHidden
173
+ } : {}),
174
+ ...(importantForAccessibility !== undefined ? {
175
+ importantForAccessibility
176
+ } : {})
177
+ });
178
+ }
179
+ return null;
180
+ }
181
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(MediaSource);
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "MediaSource", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _MediaSource.default;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "cloneChildrenWithModes", {
7
13
  enumerable: true,
8
14
  get: function () {
@@ -15,4 +21,6 @@ Object.defineProperty(exports, "flattenChildren", {
15
21
  return _reactUtils.flattenChildren;
16
22
  }
17
23
  });
18
- var _reactUtils = require("./react-utils");
24
+ var _reactUtils = require("./react-utils");
25
+ var _MediaSource = _interopRequireDefault(require("./MediaSource"));
26
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -43,7 +43,8 @@ export function Carousel({
43
43
  const gap = gapProp ?? tokenGap;
44
44
  const containerPaddingH = parseFloat(getVariableByName('carousel/padding/horizontal', modes) || '0');
45
45
  const containerPaddingV = parseFloat(getVariableByName('carousel/padding/vertical', modes) || '0');
46
- const paginationGap = parseFloat(getVariableByName('carousel/pagination/gap', modes) || '12');
46
+ // Spacing between the cards row and the pagination dots uses `carousel/gap`.
47
+ const paginationOffset = gap;
47
48
 
48
49
  // ---- Refs & state ----
49
50
  const scrollRef = useRef(null);
@@ -200,7 +201,7 @@ export function Carousel({
200
201
  }), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
201
202
  modes: modes,
202
203
  style: {
203
- marginTop: paginationGap
204
+ marginTop: paginationOffset
204
205
  }
205
206
  })]
206
207
  })
@@ -242,13 +243,15 @@ export function Pagination({
242
243
  } = useContext(CarouselContext);
243
244
  const modes = propModes || ctxModes || {};
244
245
 
245
- // Token resolution for dots
246
- const dotSize = parseFloat(getVariableByName('carousel/pagination/dotSize', modes) || '8');
247
- const dotActiveWidth = parseFloat(getVariableByName('carousel/pagination/dotActiveWidth', modes) || '24');
248
- const dotGap = parseFloat(getVariableByName('carousel/pagination/dotGap', modes) || '8');
249
- const dotColor = getVariableByName('carousel/pagination/dotColor', modes) || 'rgba(255,255,255,0.35)';
250
- const dotActiveColor = getVariableByName('carousel/pagination/dotActiveColor', modes) || '#ffffff';
251
- const dotRadius = parseFloat(getVariableByName('carousel/pagination/dotRadius', modes) || '4');
246
+ // Token resolution for dots — matches Figma tokens
247
+ // (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
248
+ // Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
249
+ const dotSize = 6;
250
+ const dotActiveWidth = 16;
251
+ const dotGap = parseFloat(getVariableByName('carousel/pagination/gap', modes) || '4');
252
+ const dotColor = getVariableByName('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
253
+ const dotActiveColor = getVariableByName('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
254
+ const dotRadius = parseFloat(getVariableByName('carousel/pagination/indicator/radius', modes) || '9999');
252
255
  const containerStyle = {
253
256
  flexDirection: 'row',
254
257
  justifyContent: 'center',
@@ -59,7 +59,8 @@ function Drawer({
59
59
  accessibilityHint,
60
60
  contentContainerStyle,
61
61
  showsVerticalScrollIndicator = false,
62
- bottomInset = 80
62
+ bottomInset = 80,
63
+ onStateChange
63
64
  }) {
64
65
  const {
65
66
  height: screenHeight
@@ -124,8 +125,13 @@ function Drawer({
124
125
 
125
126
  // Update JS state for accessibility/logic if needed
126
127
  const updateMode = useCallback(newMode => {
127
- setMode(newMode);
128
- }, []);
128
+ setMode(prev => {
129
+ if (prev !== newMode) {
130
+ onStateChange?.(newMode);
131
+ }
132
+ return newMode;
133
+ });
134
+ }, [onStateChange]);
129
135
 
130
136
  // Gesture policy:
131
137
  // • activeOffsetY: require a clear *vertical* drag (10px) before this
@@ -72,8 +72,13 @@ function resolveIconButtonTokens(modes, disabled) {
72
72
  * pressed transform mirrored via React state) — removed.
73
73
  * - Wrapped in `React.memo`.
74
74
  */
75
+ // Legacy default icon used when neither a `name` nor a `source` is supplied
76
+ // for the resolved slot. Kept as a constant rather than a destructuring
77
+ // default so source-only call sites don't accidentally render `'ic_card'`.
78
+ const LEGACY_DEFAULT_ICON_NAME = 'ic_card';
75
79
  function IconButton({
76
- iconName = 'ic_card',
80
+ iconName,
81
+ source,
77
82
  modes = EMPTY_MODES,
78
83
  onPress,
79
84
  disabled = false,
@@ -84,7 +89,9 @@ function IconButton({
84
89
  webAccessibilityProps,
85
90
  isToggle = false,
86
91
  activeIcon,
92
+ activeSource,
87
93
  inactiveIcon,
94
+ inactiveSource,
88
95
  isActive = false,
89
96
  ...rest
90
97
  }) {
@@ -107,11 +114,35 @@ function IconButton({
107
114
  userHandlersRef.current.onHoverIn = rest?.onHoverIn;
108
115
  userHandlersRef.current.onHoverOut = rest?.onHoverOut;
109
116
 
110
- // Determine which icon to display
111
- const finalIconName = isToggle ? isActive && activeIcon ? activeIcon : !isActive && inactiveIcon ? inactiveIcon : iconName : iconName;
117
+ // Resolve the active (name + source) pair for the current slot. Toggle
118
+ // mode picks active/inactive based on `isActive`; per-state overrides
119
+ // fall back to the default `iconName` / `source` when omitted. We then
120
+ // apply the legacy default icon only as a last resort, so a source-only
121
+ // call site (`<IconButton source="…" />`) renders the source instead of
122
+ // bleeding through to `'ic_card'`.
123
+ let resolvedIconName;
124
+ let resolvedSource;
125
+ if (isToggle) {
126
+ if (isActive) {
127
+ resolvedIconName = activeIcon ?? iconName;
128
+ resolvedSource = activeSource ?? source;
129
+ } else {
130
+ resolvedIconName = inactiveIcon ?? iconName;
131
+ resolvedSource = inactiveSource ?? source;
132
+ }
133
+ } else {
134
+ resolvedIconName = iconName;
135
+ resolvedSource = source;
136
+ }
137
+ if (!resolvedIconName && resolvedSource === undefined) {
138
+ resolvedIconName = LEGACY_DEFAULT_ICON_NAME;
139
+ }
112
140
 
113
- // Generate default accessibility label from icon name if not provided
114
- const defaultAccessibilityLabel = accessibilityLabel || iconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
141
+ // Generate default accessibility label from the resolved icon name when
142
+ // possible. Source-only call sites should provide an explicit
143
+ // `accessibilityLabel`; we fall back to a generic 'Icon button' so we
144
+ // never crash on `iconName.replace(...)` when only a `source` is supplied.
145
+ const defaultAccessibilityLabel = accessibilityLabel || (resolvedIconName ? resolvedIconName.replace(/^ic_/, '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Icon button');
115
146
  const webProps = usePressableWebSupport({
116
147
  restProps: rest,
117
148
  onPress: disabled ? undefined : onPress,
@@ -164,7 +195,12 @@ function IconButton({
164
195
  style: styleCallback,
165
196
  ...webProps,
166
197
  children: /*#__PURE__*/_jsx(Icon, {
167
- name: finalIconName,
198
+ ...(resolvedIconName !== undefined ? {
199
+ name: resolvedIconName
200
+ } : {}),
201
+ ...(resolvedSource !== undefined ? {
202
+ source: resolvedSource
203
+ } : {}),
168
204
  size: tokens.iconSize,
169
205
  color: tokens.iconColor,
170
206
  accessibilityElementsHidden: true,
@@ -43,6 +43,7 @@ function resolveIconCapsuleTokens(modes) {
43
43
  * @component
44
44
  * @param {Object} props - Component props
45
45
  * @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
46
+ * @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.
46
47
  * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
47
48
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
48
49
  * @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
@@ -56,6 +57,7 @@ function resolveIconCapsuleTokens(modes) {
56
57
  */
57
58
  function IconCapsule({
58
59
  iconName = 'ic_card',
60
+ source,
59
61
  modes: propModes = EMPTY_MODES,
60
62
  // accessibilityLabel is accepted on the type for API back-compat but the
61
63
  // component intentionally renders `accessibilityLabel={undefined}` (icons
@@ -85,6 +87,9 @@ function IconCapsule({
85
87
  ...rest,
86
88
  children: /*#__PURE__*/_jsx(Icon, {
87
89
  name: iconName,
90
+ ...(source !== undefined ? {
91
+ source
92
+ } : {}),
88
93
  size: tokens.iconSize,
89
94
  color: tokens.iconColor,
90
95
  accessibilityElementsHidden: true,
@@ -106,12 +106,12 @@ const Popup = /*#__PURE__*/forwardRef(function Popup({
106
106
  children: /*#__PURE__*/_jsxs(View, {
107
107
  style: styles.overlay,
108
108
  children: [/*#__PURE__*/_jsx(Animated.View, {
109
- style: [StyleSheet.absoluteFillObject, {
109
+ style: [StyleSheet.absoluteFill, {
110
110
  backgroundColor: backdropColor,
111
111
  opacity: backdropAnim
112
112
  }],
113
113
  children: /*#__PURE__*/_jsx(Pressable, {
114
- style: StyleSheet.absoluteFillObject,
114
+ style: StyleSheet.absoluteFill,
115
115
  onPress: closeOnBackdropPress ? handleClose : undefined,
116
116
  accessibilityRole: "button",
117
117
  accessibilityLabel: "Close popup"
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useCallback, useMemo, useRef, useState } from 'react';
4
- import { Pressable, View, Text, Image, Platform } from 'react-native';
4
+ import { Pressable, View, Text, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import MediaSource from '../../utils/MediaSource';
8
9
  import Icon from '../../icons/Icon';
9
10
 
10
11
  // Default static asset from the component folder.
11
- // Consumers can override the image via the `avatarSource` prop if needed.
12
+ // Consumers can override the image via the `source` prop if needed.
12
13
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
13
14
  const DEFAULT_AVATAR_IMAGE = require('./Image.png');
14
15
  const IS_WEB = Platform.OS === 'web';
@@ -83,7 +84,8 @@ function resolveUpiHandleTokens(modes) {
83
84
  * @param {Object} [props.modes={}] - Modes object passed directly to `getVariableByName`.
84
85
  * @param {boolean} [props.showIcon=true] - Toggles the trailing icon visibility.
85
86
  * @param {string} [props.iconName='ic_scan_qr_code'] - Icon name from the actions set.
86
- * @param {ImageSourcePropType} [props.avatarSource] - Optional custom image source for the avatar.
87
+ * @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.
88
+ * @param {ImageSourcePropType|UnifiedSource} [props.avatarSource] - Deprecated alias for `source`; kept for back-compat.
87
89
  * @param {Function} [props.onClick] - Click/tap handler. Works as an alias for `onPress`.
88
90
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
89
91
  * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
@@ -101,6 +103,7 @@ function UpiHandle({
101
103
  modes: propModes = EMPTY_MODES,
102
104
  showIcon = true,
103
105
  iconName = 'ic_scan_qr_code',
106
+ source,
104
107
  avatarSource,
105
108
  onPress,
106
109
  onClick,
@@ -149,13 +152,22 @@ function UpiHandle({
149
152
  pressed
150
153
  }) => [tokens.containerStyle, pressed ? pressedOverlayStyle : null, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
151
154
  const staticContainerStyle = useMemo(() => [tokens.containerStyle, isFocused ? focusOverlayStyle : null], [tokens.containerStyle, isFocused]);
155
+
156
+ // `source` wins; `avatarSource` is the legacy fallback. Both are accepted
157
+ // as a UnifiedSource (string / number / {uri} / component / element), and
158
+ // the legacy `ImageSourcePropType` shapes naturally fit that union too.
159
+ const resolvedAvatarSource = source ?? avatarSource ?? DEFAULT_AVATAR_IMAGE;
160
+ const avatarSize = tokens.avatarStyle.width ?? 23;
152
161
  const innerContent = /*#__PURE__*/_jsxs(_Fragment, {
153
- children: [/*#__PURE__*/_jsx(Image, {
154
- source: avatarSource || DEFAULT_AVATAR_IMAGE,
162
+ children: [/*#__PURE__*/_jsx(View, {
155
163
  style: tokens.avatarStyle,
156
- resizeMode: "cover",
157
- accessibilityElementsHidden: true,
158
- importantForAccessibility: "no"
164
+ children: /*#__PURE__*/_jsx(MediaSource, {
165
+ source: resolvedAvatarSource,
166
+ size: avatarSize,
167
+ resizeMode: "cover",
168
+ accessibilityElementsHidden: true,
169
+ importantForAccessibility: "no"
170
+ })
159
171
  }), /*#__PURE__*/_jsx(Text, {
160
172
  style: tokens.labelStyle,
161
173
  numberOfLines: 1,
@@ -4,97 +4,94 @@ import React from 'react';
4
4
  import { View } from 'react-native';
5
5
  import Svg, { Path } from 'react-native-svg';
6
6
  import { getIcon, hasIcon } from './registry';
7
+ import MediaSource from '../utils/MediaSource';
7
8
  import { jsx as _jsx } from "react/jsx-runtime";
8
9
  /**
9
- * Generic Icon Component
10
- *
11
- * Renders an icon from the registry by name with customizable size and color.
12
- *
13
- * @component
14
- * @param {Object} props - Component props
15
- * @param {string} props.name - Icon name from the registry (e.g., 'ic_ccv', 'ic_card')
16
- * @param {number} [props.size=24] - Icon size in pixels (width and height)
17
- * @param {string} [props.color='#141414'] - Icon color (hex, rgb, or named color)
18
- * @param {Object} [props.style] - Additional styles for the container View
19
- *
10
+ * Generic Icon component.
11
+ *
12
+ * Renders an icon from the registry by `name`, or falls back to a
13
+ * smart-detected `source` (SVG / PNG / JPG / require / SVG component /
14
+ * remote URI). External sources are tinted with `color` so they participate
15
+ * in the design-token modes just like built-in icons.
16
+ *
20
17
  * @example
21
- * ```jsx
18
+ * ```tsx
19
+ * // Built-in icon from the registry.
22
20
  * <Icon name="ic_ccv" size={24} color="#141414" />
23
- * <Icon name="ic_card" size={32} color="#5c00b5" />
24
- * <Icon name="ic_cart" size={20} color="red" />
21
+ *
22
+ * // Fallback to a remote SVG (auto-detected by the .svg extension).
23
+ * <Icon source="https://cdn.example.com/avatar.svg" size={24} color="#5c00b5" />
24
+ *
25
+ * // Fallback to a local raster asset.
26
+ * <Icon source={require('./brand.png')} size={32} />
27
+ *
28
+ * // Fallback to an SVG React component (e.g. via react-native-svg-transformer).
29
+ * import BrandLogo from './brand.svg';
30
+ * <Icon source={BrandLogo} size={24} color="red" />
25
31
  * ```
26
32
  */
27
33
  function Icon({
28
34
  name,
35
+ source,
29
36
  size = 24,
30
37
  color = '#141414',
31
38
  style,
32
39
  ...rest
33
40
  }) {
34
- // Validate icon name
35
- if (!name) {
36
- console.warn('Icon: name prop is required');
37
- return null;
38
- }
39
- if (!hasIcon(name)) {
40
- const {
41
- getIconNames
42
- } = require('./registry');
43
- console.warn(`Icon: "${name}" not found in registry. Available icons: ${getIconNames().join(', ')}`);
44
- return null;
45
- }
46
-
47
- // Get icon data from registry
48
- const iconData = getIcon(name);
49
- if (!iconData) {
50
- return null;
51
- }
52
-
53
- // Parse viewBox to get width and height for aspect ratio
54
- const viewBoxParts = iconData.viewBox.split(' ');
55
- // @ts-ignore
56
- const viewBoxWidth = parseFloat(viewBoxParts[2]) || size;
57
- // @ts-ignore
58
- const viewBoxHeight = parseFloat(viewBoxParts[3]) || size;
59
-
60
- // Calculate aspect ratio to maintain proper scaling
61
- const aspectRatio = viewBoxWidth / viewBoxHeight;
62
-
63
- // Determine actual width and height based on size and aspect ratio
64
- let width = size;
65
- let height = size;
66
-
67
- // If viewBox is not square, adjust dimensions to maintain aspect ratio
68
- if (Math.abs(aspectRatio - 1) > 0.01) {
69
- if (aspectRatio > 1) {
70
- // Wider than tall
71
- height = size / aspectRatio;
72
- } else {
73
- // Taller than wide
74
- width = size * aspectRatio;
75
- }
76
- }
77
- const containerStyle = {
41
+ const containerStyle = [{
78
42
  width: size,
79
43
  height: size,
80
44
  alignItems: 'center',
81
- justifyContent: 'center',
82
- ...style
83
- };
84
- return /*#__PURE__*/_jsx(View, {
85
- style: containerStyle,
86
- ...rest,
87
- children: /*#__PURE__*/_jsx(Svg, {
88
- width: width,
89
- height: height,
90
- viewBox: iconData.viewBox,
91
- preserveAspectRatio: "xMidYMid meet",
92
- children: /*#__PURE__*/_jsx(Path, {
93
- d: iconData.path,
94
- fill: color,
95
- fillRule: iconData.fillRule || 'nonzero'
45
+ justifyContent: 'center'
46
+ }, style];
47
+ const iconData = name && hasIcon(name) ? getIcon(name) : null;
48
+ if (iconData) {
49
+ const viewBoxParts = iconData.viewBox.split(' ');
50
+ const viewBoxWidth = parseFloat(viewBoxParts[2] ?? `${size}`) || size;
51
+ const viewBoxHeight = parseFloat(viewBoxParts[3] ?? `${size}`) || size;
52
+ const aspectRatio = viewBoxWidth / viewBoxHeight;
53
+ let width = size;
54
+ let height = size;
55
+ if (Math.abs(aspectRatio - 1) > 0.01) {
56
+ if (aspectRatio > 1) {
57
+ height = size / aspectRatio;
58
+ } else {
59
+ width = size * aspectRatio;
60
+ }
61
+ }
62
+ return /*#__PURE__*/_jsx(View, {
63
+ style: containerStyle,
64
+ ...rest,
65
+ children: /*#__PURE__*/_jsx(Svg, {
66
+ width: width,
67
+ height: height,
68
+ viewBox: iconData.viewBox,
69
+ preserveAspectRatio: "xMidYMid meet",
70
+ children: /*#__PURE__*/_jsx(Path, {
71
+ d: iconData.path,
72
+ fill: color,
73
+ fillRule: iconData.fillRule || 'nonzero'
74
+ })
75
+ })
76
+ });
77
+ }
78
+ if (source !== undefined) {
79
+ return /*#__PURE__*/_jsx(View, {
80
+ style: containerStyle,
81
+ ...rest,
82
+ children: /*#__PURE__*/_jsx(MediaSource, {
83
+ source: source,
84
+ size: size,
85
+ tintColor: color,
86
+ resizeMode: "contain"
96
87
  })
97
- })
98
- });
88
+ });
89
+ }
90
+ if (!name) {
91
+ console.warn('Icon: either `name` or `source` is required');
92
+ return null;
93
+ }
94
+ console.warn(`Icon: "${name}" not found in registry and no \`source\` fallback was provided.`);
95
+ return null;
99
96
  }
100
97
  export default Icon;