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.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commonjs/components/CardCTA/CardCTA.js +15 -1
  3. package/lib/commonjs/components/Carousel/Carousel.js +34 -13
  4. package/lib/commonjs/components/Drawer/Drawer.js +9 -3
  5. package/lib/commonjs/components/IconButton/IconButton.js +42 -6
  6. package/lib/commonjs/components/IconCapsule/IconCapsule.js +5 -0
  7. package/lib/commonjs/components/Popup/Popup.js +2 -2
  8. package/lib/commonjs/components/Section/Section.js +22 -7
  9. package/lib/commonjs/components/UpiHandle/UpiHandle.js +19 -7
  10. package/lib/commonjs/icons/Icon.js +72 -75
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/commonjs/utils/MediaSource.js +181 -0
  13. package/lib/commonjs/utils/index.js +9 -1
  14. package/lib/module/components/CardCTA/CardCTA.js +15 -1
  15. package/lib/module/components/Carousel/Carousel.js +34 -13
  16. package/lib/module/components/Drawer/Drawer.js +9 -3
  17. package/lib/module/components/IconButton/IconButton.js +42 -6
  18. package/lib/module/components/IconCapsule/IconCapsule.js +5 -0
  19. package/lib/module/components/Popup/Popup.js +2 -2
  20. package/lib/module/components/Section/Section.js +23 -8
  21. package/lib/module/components/UpiHandle/UpiHandle.js +20 -8
  22. package/lib/module/icons/Icon.js +72 -75
  23. package/lib/module/icons/registry.js +1 -1
  24. package/lib/module/utils/MediaSource.js +176 -0
  25. package/lib/module/utils/index.js +2 -1
  26. package/lib/typescript/src/components/Drawer/Drawer.d.ts +6 -1
  27. package/lib/typescript/src/components/IconButton/IconButton.d.ts +25 -14
  28. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +12 -1
  29. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +17 -3
  30. package/lib/typescript/src/icons/Icon.d.ts +35 -16
  31. package/lib/typescript/src/icons/registry.d.ts +1 -1
  32. package/lib/typescript/src/utils/MediaSource.d.ts +63 -0
  33. package/lib/typescript/src/utils/index.d.ts +2 -0
  34. package/package.json +1 -1
  35. package/src/components/CardCTA/CardCTA.tsx +13 -0
  36. package/src/components/Carousel/Carousel.tsx +37 -20
  37. package/src/components/Drawer/Drawer.tsx +13 -2
  38. package/src/components/IconButton/IconButton.tsx +70 -11
  39. package/src/components/IconCapsule/IconCapsule.tsx +13 -0
  40. package/src/components/Popup/Popup.tsx +2 -2
  41. package/src/components/Section/Section.tsx +29 -12
  42. package/src/components/UpiHandle/UpiHandle.tsx +37 -11
  43. package/src/icons/Icon.tsx +91 -76
  44. package/src/icons/registry.ts +1 -1
  45. package/src/utils/MediaSource.tsx +220 -0
  46. 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
- 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);
@@ -180,8 +181,26 @@ export function Carousel({
180
181
  onScrollBeginDrag: handleScrollBeginDrag,
181
182
  onScrollEndDrag: handleScrollEndDrag,
182
183
  children: items.map((child, index) => {
183
- const itemStyle = {
184
- width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined
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: [itemStyle, child.props?.style]
212
+ style: [childOverrideStyle, child.props?.style]
194
213
  }) : child;
195
214
  return /*#__PURE__*/_jsx(View, {
196
- style: itemStyle,
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: paginationGap
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
- 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');
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(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"
@@ -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, withSpring } from 'react-native-reanimated';
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) springs
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 = withSpring(h, {
183
- damping: 22,
184
- stiffness: 180,
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.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(ReduceMotion.System);
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, 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,