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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.0.65] - 2026-04-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Drawer` state callback:** Optional `onStateChange?: (state: 'collapsed' | 'expanded') => void` runs when the drawer settles into a new snap state (after gestures), so parents can react programmatically.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
7
15
|
## [0.0.64] - 2026-04-20
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -61,8 +61,17 @@ function CardCTA({
|
|
|
61
61
|
flexDirection: 'row',
|
|
62
62
|
overflow: 'hidden'
|
|
63
63
|
};
|
|
64
|
+
|
|
65
|
+
// NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
|
|
66
|
+
// Without them, Yoga's default `min-width: auto` clamps leftWrap to its
|
|
67
|
+
// single-line intrinsic text width, which steals all space from rightWrap
|
|
68
|
+
// and pushes the IconCapsule outside the card. See: text-not-wrapping
|
|
69
|
+
// inside flex rows on RN.
|
|
64
70
|
const leftWrapStyle = {
|
|
65
71
|
flex: 3,
|
|
72
|
+
flexShrink: 1,
|
|
73
|
+
flexBasis: 0,
|
|
74
|
+
minWidth: 0,
|
|
66
75
|
paddingHorizontal: leftPaddingH,
|
|
67
76
|
paddingVertical: leftPaddingV,
|
|
68
77
|
gap: leftGap,
|
|
@@ -71,13 +80,18 @@ function CardCTA({
|
|
|
71
80
|
};
|
|
72
81
|
const rightWrapStyle = {
|
|
73
82
|
flex: 2,
|
|
83
|
+
flexShrink: 1,
|
|
84
|
+
flexBasis: 0,
|
|
85
|
+
minWidth: 0,
|
|
74
86
|
paddingHorizontal: rightPaddingH,
|
|
75
87
|
paddingVertical: rightPaddingV,
|
|
76
88
|
alignItems: 'flex-end',
|
|
77
89
|
justifyContent: 'flex-start'
|
|
78
90
|
};
|
|
79
91
|
const textWrapStyle = {
|
|
80
|
-
gap: textGap
|
|
92
|
+
gap: textGap,
|
|
93
|
+
alignSelf: 'stretch',
|
|
94
|
+
minWidth: 0
|
|
81
95
|
};
|
|
82
96
|
const titleStyle = {
|
|
83
97
|
color: titleColor,
|
|
@@ -51,7 +51,8 @@ function Carousel({
|
|
|
51
51
|
const gap = gapProp ?? tokenGap;
|
|
52
52
|
const containerPaddingH = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/horizontal', modes) || '0');
|
|
53
53
|
const containerPaddingV = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/vertical', modes) || '0');
|
|
54
|
-
|
|
54
|
+
// Spacing between the cards row and the pagination dots uses `carousel/gap`.
|
|
55
|
+
const paginationOffset = gap;
|
|
55
56
|
|
|
56
57
|
// ---- Refs & state ----
|
|
57
58
|
const scrollRef = (0, _react.useRef)(null);
|
|
@@ -188,8 +189,26 @@ function Carousel({
|
|
|
188
189
|
onScrollBeginDrag: handleScrollBeginDrag,
|
|
189
190
|
onScrollEndDrag: handleScrollEndDrag,
|
|
190
191
|
children: items.map((child, index) => {
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
// Strict slot box: width must be honored; never grow or shrink with
|
|
193
|
+
// content, and clip anything that misbehaves (e.g. a child whose
|
|
194
|
+
// inner flex layout would otherwise leak into the next slot on
|
|
195
|
+
// native).
|
|
196
|
+
const slotStyle = {
|
|
197
|
+
width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
198
|
+
flexGrow: 0,
|
|
199
|
+
flexShrink: 0,
|
|
200
|
+
flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
|
|
201
|
+
overflow: 'hidden'
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// The cloned style forces the child's outer node to also honor the
|
|
205
|
+
// slot width strictly. Without this, a child with a weird intrinsic
|
|
206
|
+
// size can render wider than the slot and visually overflow.
|
|
207
|
+
const childOverrideStyle = {
|
|
208
|
+
width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
209
|
+
maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
210
|
+
flexGrow: 0,
|
|
211
|
+
flexShrink: 0
|
|
193
212
|
};
|
|
194
213
|
|
|
195
214
|
// Pass modes down to children
|
|
@@ -198,17 +217,17 @@ function Carousel({
|
|
|
198
217
|
...(child.props?.modes || {}),
|
|
199
218
|
...modes
|
|
200
219
|
},
|
|
201
|
-
style: [
|
|
220
|
+
style: [childOverrideStyle, child.props?.style]
|
|
202
221
|
}) : child;
|
|
203
222
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
204
|
-
style:
|
|
223
|
+
style: slotStyle,
|
|
205
224
|
children: childWithModes
|
|
206
225
|
}, index);
|
|
207
226
|
})
|
|
208
227
|
}), showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
|
|
209
228
|
modes: modes,
|
|
210
229
|
style: {
|
|
211
|
-
marginTop:
|
|
230
|
+
marginTop: paginationOffset
|
|
212
231
|
}
|
|
213
232
|
})]
|
|
214
233
|
})
|
|
@@ -250,13 +269,15 @@ function Pagination({
|
|
|
250
269
|
} = (0, _react.useContext)(CarouselContext);
|
|
251
270
|
const modes = propModes || ctxModes || {};
|
|
252
271
|
|
|
253
|
-
// Token resolution for dots
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
272
|
+
// Token resolution for dots — matches Figma tokens
|
|
273
|
+
// (carousel/pagination/gap, carousel/pagination/indicator/{activecolor,inactivecolor,radius}).
|
|
274
|
+
// Dot dimensions are fixed per Figma spec: inactive 6x6, active 16x6.
|
|
275
|
+
const dotSize = 6;
|
|
276
|
+
const dotActiveWidth = 16;
|
|
277
|
+
const dotGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/gap', modes) || '4');
|
|
278
|
+
const dotColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/inactivecolor', modes) || 'rgba(0,0,0,0.3)';
|
|
279
|
+
const dotActiveColor = (0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/activecolor', modes) || '#170d0a';
|
|
280
|
+
const dotRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/pagination/indicator/radius', modes) || '9999');
|
|
260
281
|
const containerStyle = {
|
|
261
282
|
flexDirection: 'row',
|
|
262
283
|
justifyContent: 'center',
|
|
@@ -64,7 +64,8 @@ function Drawer({
|
|
|
64
64
|
accessibilityHint,
|
|
65
65
|
contentContainerStyle,
|
|
66
66
|
showsVerticalScrollIndicator = false,
|
|
67
|
-
bottomInset = 80
|
|
67
|
+
bottomInset = 80,
|
|
68
|
+
onStateChange
|
|
68
69
|
}) {
|
|
69
70
|
const {
|
|
70
71
|
height: screenHeight
|
|
@@ -129,8 +130,13 @@ function Drawer({
|
|
|
129
130
|
|
|
130
131
|
// Update JS state for accessibility/logic if needed
|
|
131
132
|
const updateMode = (0, _react.useCallback)(newMode => {
|
|
132
|
-
setMode(
|
|
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
|
|
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
|
-
//
|
|
117
|
-
|
|
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
|
|
120
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
120
|
+
style: _reactNative.StyleSheet.absoluteFill,
|
|
121
121
|
onPress: closeOnBackdropPress ? handleClose : undefined,
|
|
122
122
|
accessibilityRole: "button",
|
|
123
123
|
accessibilityLabel: "Close popup"
|
|
@@ -90,7 +90,14 @@ const SLOT_GRID_MAX_COLUMNS = 4;
|
|
|
90
90
|
const SLOT_GRID_STAGGER_CAP = 8;
|
|
91
91
|
const SLOT_GRID_ENTER_STAGGER_MS = 35;
|
|
92
92
|
const SLOT_GRID_EXIT_STAGGER_MS = 20;
|
|
93
|
+
const SLOT_GRID_ENTER_DURATION_MS = 220;
|
|
93
94
|
const SLOT_GRID_EXIT_DURATION_MS = 160;
|
|
95
|
+
const SLOT_GRID_HEIGHT_DURATION_MS = 280;
|
|
96
|
+
|
|
97
|
+
// Standard ease-out cubic curve. Calm, professional, no overshoot — matches
|
|
98
|
+
// system-style transitions. Defined once at module scope so it isn't
|
|
99
|
+
// re-allocated per render.
|
|
100
|
+
const SLOT_GRID_EASING = _reactNativeReanimated.Easing.out(_reactNativeReanimated.Easing.cubic);
|
|
94
101
|
const slotGridRowFlowStyle = {
|
|
95
102
|
flexDirection: 'row',
|
|
96
103
|
justifyContent: 'space-between'
|
|
@@ -136,6 +143,13 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
|
|
|
136
143
|
const containerStyle = (0, _react.useMemo)(() => ({
|
|
137
144
|
gap
|
|
138
145
|
}), [gap]);
|
|
146
|
+
// Strict `width` (not `minWidth`) so every cell in every row is exactly the
|
|
147
|
+
// same size — `space-between` then distributes identical leftover into
|
|
148
|
+
// identical inter-cell gaps on every row, which keeps column N of row 1
|
|
149
|
+
// aligned with column N of rows 2/3/etc. Cells whose label is wider than
|
|
150
|
+
// `cellWidth` simply wrap their text onto more lines (taking more vertical
|
|
151
|
+
// space; the row's height grows naturally to fit the tallest cell, and the
|
|
152
|
+
// animated-height clip springs to the new total).
|
|
139
153
|
const cellStyle = (0, _react.useMemo)(() => cellWidth !== null ? {
|
|
140
154
|
width: cellWidth
|
|
141
155
|
} : undefined, [cellWidth]);
|
|
@@ -169,8 +183,9 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
|
|
|
169
183
|
// and an explicit `height` driven by a shared value.
|
|
170
184
|
// 3. The inner view reports its natural height via `onLayout`. The first
|
|
171
185
|
// measurement snaps the shared value (no first-mount animation). Every
|
|
172
|
-
// subsequent change (e.g. expand/collapse adds or removes rows)
|
|
173
|
-
// the shared value to the new natural height
|
|
186
|
+
// subsequent change (e.g. expand/collapse adds or removes rows) eases
|
|
187
|
+
// the shared value to the new natural height with a calm ease-out
|
|
188
|
+
// timing curve — no spring, no bounce, no overshoot.
|
|
174
189
|
//
|
|
175
190
|
// Visually: the container reveals/conceals content like a curtain, and the
|
|
176
191
|
// cells never deform.
|
|
@@ -184,9 +199,9 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
|
|
|
184
199
|
animatedHeight.value = h;
|
|
185
200
|
return;
|
|
186
201
|
}
|
|
187
|
-
animatedHeight.value = (0, _reactNativeReanimated.
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
animatedHeight.value = (0, _reactNativeReanimated.withTiming)(h, {
|
|
203
|
+
duration: SLOT_GRID_HEIGHT_DURATION_MS,
|
|
204
|
+
easing: SLOT_GRID_EASING,
|
|
190
205
|
reduceMotion: _reactNativeReanimated.ReduceMotion.System
|
|
191
206
|
});
|
|
192
207
|
}, [animatedHeight]);
|
|
@@ -210,8 +225,8 @@ const SlotGrid = /*#__PURE__*/_react.default.memo(function SlotGrid({
|
|
|
210
225
|
const enterStaggerSteps = Math.min(extraOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
211
226
|
const reverseOrdinal = Math.max(0, extrasCount - 1 - extraOrdinal);
|
|
212
227
|
const exitStaggerSteps = Math.min(reverseOrdinal, SLOT_GRID_STAGGER_CAP);
|
|
213
|
-
const entering = _reactNativeReanimated.FadeInUp.
|
|
214
|
-
const exiting = _reactNativeReanimated.FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
|
|
228
|
+
const entering = _reactNativeReanimated.FadeInUp.duration(SLOT_GRID_ENTER_DURATION_MS).easing(SLOT_GRID_EASING).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
|
|
229
|
+
const exiting = _reactNativeReanimated.FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS).easing(SLOT_GRID_EASING).delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
|
|
215
230
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
216
231
|
entering: entering,
|
|
217
232
|
exiting: exiting,
|
|
@@ -9,12 +9,13 @@ var _reactNative = require("react-native");
|
|
|
9
9
|
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
10
10
|
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
11
11
|
var _reactUtils = require("../../utils/react-utils");
|
|
12
|
+
var _MediaSource = _interopRequireDefault(require("../../utils/MediaSource"));
|
|
12
13
|
var _Icon = _interopRequireDefault(require("../../icons/Icon"));
|
|
13
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
14
15
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
16
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
16
17
|
// Default static asset from the component folder.
|
|
17
|
-
// Consumers can override the image via the `
|
|
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 {
|
|
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.
|
|
159
|
-
source: avatarSource || DEFAULT_AVATAR_IMAGE,
|
|
167
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
160
168
|
style: tokens.avatarStyle,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
16
|
-
*
|
|
17
|
-
* Renders an icon from the registry by name
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
* ```
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // Built-in icon from the registry.
|
|
28
26
|
* <Icon name="ic_ccv" size={24} color="#141414" />
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|