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.
- package/CHANGELOG.md +8 -0
- package/lib/commonjs/components/Carousel/Carousel.js +12 -9
- package/lib/commonjs/components/Drawer/Drawer.js +9 -3
- package/lib/commonjs/components/IconButton/IconButton.js +42 -6
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
- package/lib/commonjs/components/Popup/Popup.js +2 -2
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
- package/lib/commonjs/icons/Icon.js +72 -75
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/MediaSource.js +181 -0
- package/lib/commonjs/utils/index.js +9 -1
- package/lib/module/components/Carousel/Carousel.js +12 -9
- package/lib/module/components/Drawer/Drawer.js +9 -3
- package/lib/module/components/IconButton/IconButton.js +42 -6
- package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
- package/lib/module/components/Popup/Popup.js +2 -2
- package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
- package/lib/module/icons/Icon.js +72 -75
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/MediaSource.js +176 -0
- package/lib/module/utils/index.js +2 -1
- package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
- package/lib/typescript/src/icons/Icon.d.ts +35 -16
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
- package/lib/typescript/src/utils/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/Carousel/Carousel.tsx +16 -17
- package/src/components/Drawer/Drawer.tsx +13 -2
- package/src/components/IconButton/IconButton.tsx +70 -11
- package/src/components/IconCapsule/IconCapsule.tsx +13 -0
- package/src/components/Popup/Popup.tsx +2 -2
- package/src/components/UpiHandle/UpiHandle.tsx +37 -11
- package/src/icons/Icon.tsx +91 -76
- package/src/icons/registry.ts +1 -1
- package/src/utils/MediaSource.tsx +220 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
111
|
-
|
|
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
|
|
114
|
-
|
|
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
|
-
|
|
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.
|
|
109
|
+
style: [StyleSheet.absoluteFill, {
|
|
110
110
|
backgroundColor: backdropColor,
|
|
111
111
|
opacity: backdropAnim
|
|
112
112
|
}],
|
|
113
113
|
children: /*#__PURE__*/_jsx(Pressable, {
|
|
114
|
-
style: StyleSheet.
|
|
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,
|
|
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 `
|
|
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 {
|
|
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(
|
|
154
|
-
source: avatarSource || DEFAULT_AVATAR_IMAGE,
|
|
162
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
155
163
|
style: tokens.avatarStyle,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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,
|
package/lib/module/icons/Icon.js
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
11
|
-
* Renders an icon from the registry by name
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
* ```
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // Built-in icon from the registry.
|
|
22
20
|
* <Icon name="ic_ccv" size={24} color="#141414" />
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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;
|