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.
- package/CHANGELOG.md +8 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +15 -1
- package/lib/commonjs/components/Carousel/Carousel.js +34 -13
- 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/Section/Section.js +22 -7
- 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/CardCTA/CardCTA.js +15 -1
- package/lib/module/components/Carousel/Carousel.js +34 -13
- 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/Section/Section.js +23 -8
- 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/CardCTA/CardCTA.tsx +13 -0
- package/src/components/Carousel/Carousel.tsx +37 -20
- 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/Section/Section.tsx +29 -12
- 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 }; }
|
|
@@ -56,8 +56,17 @@ function CardCTA({
|
|
|
56
56
|
flexDirection: 'row',
|
|
57
57
|
overflow: 'hidden'
|
|
58
58
|
};
|
|
59
|
+
|
|
60
|
+
// NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
|
|
61
|
+
// Without them, Yoga's default `min-width: auto` clamps leftWrap to its
|
|
62
|
+
// single-line intrinsic text width, which steals all space from rightWrap
|
|
63
|
+
// and pushes the IconCapsule outside the card. See: text-not-wrapping
|
|
64
|
+
// inside flex rows on RN.
|
|
59
65
|
const leftWrapStyle = {
|
|
60
66
|
flex: 3,
|
|
67
|
+
flexShrink: 1,
|
|
68
|
+
flexBasis: 0,
|
|
69
|
+
minWidth: 0,
|
|
61
70
|
paddingHorizontal: leftPaddingH,
|
|
62
71
|
paddingVertical: leftPaddingV,
|
|
63
72
|
gap: leftGap,
|
|
@@ -66,13 +75,18 @@ function CardCTA({
|
|
|
66
75
|
};
|
|
67
76
|
const rightWrapStyle = {
|
|
68
77
|
flex: 2,
|
|
78
|
+
flexShrink: 1,
|
|
79
|
+
flexBasis: 0,
|
|
80
|
+
minWidth: 0,
|
|
69
81
|
paddingHorizontal: rightPaddingH,
|
|
70
82
|
paddingVertical: rightPaddingV,
|
|
71
83
|
alignItems: 'flex-end',
|
|
72
84
|
justifyContent: 'flex-start'
|
|
73
85
|
};
|
|
74
86
|
const textWrapStyle = {
|
|
75
|
-
gap: textGap
|
|
87
|
+
gap: textGap,
|
|
88
|
+
alignSelf: 'stretch',
|
|
89
|
+
minWidth: 0
|
|
76
90
|
};
|
|
77
91
|
const titleStyle = {
|
|
78
92
|
color: titleColor,
|
|
@@ -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);
|
|
@@ -180,8 +181,26 @@ export function Carousel({
|
|
|
180
181
|
onScrollBeginDrag: handleScrollBeginDrag,
|
|
181
182
|
onScrollEndDrag: handleScrollEndDrag,
|
|
182
183
|
children: items.map((child, index) => {
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
// Strict slot box: width must be honored; never grow or shrink with
|
|
185
|
+
// content, and clip anything that misbehaves (e.g. a child whose
|
|
186
|
+
// inner flex layout would otherwise leak into the next slot on
|
|
187
|
+
// native).
|
|
188
|
+
const slotStyle = {
|
|
189
|
+
width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
190
|
+
flexGrow: 0,
|
|
191
|
+
flexShrink: 0,
|
|
192
|
+
flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
|
|
193
|
+
overflow: 'hidden'
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// The cloned style forces the child's outer node to also honor the
|
|
197
|
+
// slot width strictly. Without this, a child with a weird intrinsic
|
|
198
|
+
// size can render wider than the slot and visually overflow.
|
|
199
|
+
const childOverrideStyle = {
|
|
200
|
+
width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
201
|
+
maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
202
|
+
flexGrow: 0,
|
|
203
|
+
flexShrink: 0
|
|
185
204
|
};
|
|
186
205
|
|
|
187
206
|
// Pass modes down to children
|
|
@@ -190,17 +209,17 @@ export function Carousel({
|
|
|
190
209
|
...(child.props?.modes || {}),
|
|
191
210
|
...modes
|
|
192
211
|
},
|
|
193
|
-
style: [
|
|
212
|
+
style: [childOverrideStyle, child.props?.style]
|
|
194
213
|
}) : child;
|
|
195
214
|
return /*#__PURE__*/_jsx(View, {
|
|
196
|
-
style:
|
|
215
|
+
style: slotStyle,
|
|
197
216
|
children: childWithModes
|
|
198
217
|
}, index);
|
|
199
218
|
})
|
|
200
219
|
}), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
|
|
201
220
|
modes: modes,
|
|
202
221
|
style: {
|
|
203
|
-
marginTop:
|
|
222
|
+
marginTop: paginationOffset
|
|
204
223
|
}
|
|
205
224
|
})]
|
|
206
225
|
})
|
|
@@ -242,13 +261,15 @@ export function Pagination({
|
|
|
242
261
|
} = useContext(CarouselContext);
|
|
243
262
|
const modes = propModes || ctxModes || {};
|
|
244
263
|
|
|
245
|
-
// Token resolution for dots
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
264
|
+
// Token resolution for dots — matches Figma tokens
|
|
265
|
+
// (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
|
|
266
|
+
// Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
|
|
267
|
+
const dotSize = 6;
|
|
268
|
+
const dotActiveWidth = 16;
|
|
269
|
+
const dotGap = parseFloat(getVariableByName('carousel/pagination/gap', modes) || '4');
|
|
270
|
+
const dotColor = getVariableByName('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
|
|
271
|
+
const dotActiveColor = getVariableByName('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
|
|
272
|
+
const dotRadius = parseFloat(getVariableByName('carousel/pagination/indicator/radius', modes) || '9999');
|
|
252
273
|
const containerStyle = {
|
|
253
274
|
flexDirection: 'row',
|
|
254
275
|
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"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useMemo, useRef, useCallback } from 'react';
|
|
4
4
|
import { View, Text, Pressable, Platform } from 'react-native';
|
|
5
|
-
import Animated, { FadeInUp, FadeOutUp, ReduceMotion, useAnimatedStyle, useSharedValue,
|
|
5
|
+
import Animated, { Easing, FadeInUp, FadeOutUp, ReduceMotion, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
6
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
7
7
|
import NavArrow from '../NavArrow/NavArrow';
|
|
8
8
|
import IconCapsule from '../IconCapsule/IconCapsule';
|
|
@@ -85,7 +85,14 @@ const SLOT_GRID_MAX_COLUMNS = 4;
|
|
|
85
85
|
const SLOT_GRID_STAGGER_CAP = 8;
|
|
86
86
|
const SLOT_GRID_ENTER_STAGGER_MS = 35;
|
|
87
87
|
const SLOT_GRID_EXIT_STAGGER_MS = 20;
|
|
88
|
+
const SLOT_GRID_ENTER_DURATION_MS = 220;
|
|
88
89
|
const SLOT_GRID_EXIT_DURATION_MS = 160;
|
|
90
|
+
const SLOT_GRID_HEIGHT_DURATION_MS = 280;
|
|
91
|
+
|
|
92
|
+
// Standard ease-out cubic curve. Calm, professional, no overshoot — matches
|
|
93
|
+
// system-style transitions. Defined once at module scope so it isn't
|
|
94
|
+
// re-allocated per render.
|
|
95
|
+
const SLOT_GRID_EASING = Easing.out(Easing.cubic);
|
|
89
96
|
const slotGridRowFlowStyle = {
|
|
90
97
|
flexDirection: 'row',
|
|
91
98
|
justifyContent: 'space-between'
|
|
@@ -131,6 +138,13 @@ const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
|
|
|
131
138
|
const containerStyle = useMemo(() => ({
|
|
132
139
|
gap
|
|
133
140
|
}), [gap]);
|
|
141
|
+
// Strict `width` (not `minWidth`) so every cell in every row is exactly the
|
|
142
|
+
// same size — `space-between` then distributes identical leftover into
|
|
143
|
+
// identical inter-cell gaps on every row, which keeps column N of row 1
|
|
144
|
+
// aligned with column N of rows 2/3/etc. Cells whose label is wider than
|
|
145
|
+
// `cellWidth` simply wrap their text onto more lines (taking more vertical
|
|
146
|
+
// space; the row's height grows naturally to fit the tallest cell, and the
|
|
147
|
+
// animated-height clip springs to the new total).
|
|
134
148
|
const cellStyle = useMemo(() => cellWidth !== null ? {
|
|
135
149
|
width: cellWidth
|
|
136
150
|
} : undefined, [cellWidth]);
|
|
@@ -164,8 +178,9 @@ const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
|
|
|
164
178
|
// and an explicit `height` driven by a shared value.
|
|
165
179
|
// 3. The inner view reports its natural height via `onLayout`. The first
|
|
166
180
|
// measurement snaps the shared value (no first-mount animation). Every
|
|
167
|
-
// subsequent change (e.g. expand/collapse adds or removes rows)
|
|
168
|
-
// the shared value to the new natural height
|
|
181
|
+
// subsequent change (e.g. expand/collapse adds or removes rows) eases
|
|
182
|
+
// the shared value to the new natural height with a calm ease-out
|
|
183
|
+
// timing curve — no spring, no bounce, no overshoot.
|
|
169
184
|
//
|
|
170
185
|
// Visually: the container reveals/conceals content like a curtain, and the
|
|
171
186
|
// cells never deform.
|
|
@@ -179,9 +194,9 @@ const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
|
|
|
179
194
|
animatedHeight.value = h;
|
|
180
195
|
return;
|
|
181
196
|
}
|
|
182
|
-
animatedHeight.value =
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
animatedHeight.value = withTiming(h, {
|
|
198
|
+
duration: SLOT_GRID_HEIGHT_DURATION_MS,
|
|
199
|
+
easing: SLOT_GRID_EASING,
|
|
185
200
|
reduceMotion: ReduceMotion.System
|
|
186
201
|
});
|
|
187
202
|
}, [animatedHeight]);
|
|
@@ -205,8 +220,8 @@ const SlotGrid = /*#__PURE__*/React.memo(function SlotGrid({
|
|
|
205
220
|
const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
206
221
|
const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
|
|
207
222
|
const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
208
|
-
const entering = FadeInUp.
|
|
209
|
-
const exiting = FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(ReduceMotion.System);
|
|
223
|
+
const entering = FadeInUp.duration(SLOT_GRID_ENTER_DURATION_MS).easing(SLOT_GRID_EASING).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(ReduceMotion.System);
|
|
224
|
+
const exiting = FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).easing(SLOT_GRID_EASING).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(ReduceMotion.System);
|
|
210
225
|
return /*#__PURE__*/_jsx(Animated.View, {
|
|
211
226
|
entering: entering,
|
|
212
227
|
exiting: exiting,
|
|
@@ -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,
|