jfs-components 0.0.65 → 0.0.67

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 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.67] - 2026-04-22
8
+
9
+ ### Fixed
10
+
11
+ - **`MediaCard` in `Carousel` (Yoga / Android):** `MediaCard.Header` no longer uses `flex: 1` shorthand (`flexBasis: 0`), which made the header size to zero on the first layout pass when the card sat in a height-unbounded parent (e.g. horizontal carousel content). Replaced with explicit `flexGrow: 1`, `flexShrink: 0`, `flexBasis: 'auto'` so the header has a content-based floor, matching the Yoga fix pattern used for `CardCTA` right-wrap.
12
+
13
+ ---
14
+
7
15
  ## [0.0.65] - 2026-04-21
8
16
 
9
17
  ### Added
@@ -61,23 +61,49 @@ 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,
69
78
  alignItems: 'flex-start',
70
79
  justifyContent: 'center'
71
80
  };
81
+
82
+ // NOTE: rightWrap must NOT shrink on native. On Android (Yoga), the default
83
+ // `flex: 2` shorthand expands to `{ flexGrow: 2, flexShrink: 1, flexBasis: 0 }`,
84
+ // which — combined with `minWidth: 0` — lets Yoga shrink this wrapper below
85
+ // its own padding+IconCapsule width when leftWrap's text is long. Once that
86
+ // happens the horizontal padding collapses, `alignItems: 'flex-end'` pins
87
+ // the IconCapsule against the inner edge, and the icon ends up visually
88
+ // touching the body text (the wrapper "appears not to exist"). Web hides
89
+ // this because browsers honor `min-width: auto` on flex items. Use
90
+ // explicit `flexGrow`/`flexShrink: 0`/`flexBasis: 'auto'` so the wrapper
91
+ // is sized to its content as a floor and only grows for the design's
92
+ // 3:2 ratio when extra space is available. leftWrap already absorbs tight
93
+ // space via `flexShrink: 1` + `minWidth: 0`.
72
94
  const rightWrapStyle = {
73
- flex: 2,
95
+ flexGrow: 2,
96
+ flexShrink: 0,
97
+ flexBasis: 'auto',
74
98
  paddingHorizontal: rightPaddingH,
75
99
  paddingVertical: rightPaddingV,
76
100
  alignItems: 'flex-end',
77
101
  justifyContent: 'flex-start'
78
102
  };
79
103
  const textWrapStyle = {
80
- gap: textGap
104
+ gap: textGap,
105
+ alignSelf: 'stretch',
106
+ minWidth: 0
81
107
  };
82
108
  const titleStyle = {
83
109
  color: titleColor,
@@ -189,8 +189,26 @@ function Carousel({
189
189
  onScrollBeginDrag: handleScrollBeginDrag,
190
190
  onScrollEndDrag: handleScrollEndDrag,
191
191
  children: items.map((child, index) => {
192
- const itemStyle = {
193
- width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined
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
194
212
  };
195
213
 
196
214
  // Pass modes down to children
@@ -199,10 +217,10 @@ function Carousel({
199
217
  ...(child.props?.modes || {}),
200
218
  ...modes
201
219
  },
202
- style: [itemStyle, child.props?.style]
220
+ style: [childOverrideStyle, child.props?.style]
203
221
  }) : child;
204
222
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
205
- style: itemStyle,
223
+ style: slotStyle,
206
224
  children: childWithModes
207
225
  }, index);
208
226
  })
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _jsxRuntime = require("react/jsx-runtime");
10
+ 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); }
11
+ function normalizeSource(imageSource) {
12
+ if (imageSource == null) return undefined;
13
+ if (typeof imageSource === 'string') return {
14
+ uri: imageSource
15
+ };
16
+ return imageSource;
17
+ }
18
+
19
+ /**
20
+ * `Image` — the library's standard raster image primitive.
21
+ *
22
+ * Why this exists:
23
+ * - Gives consumers a single component for "show an image at a given aspect
24
+ * ratio inside a flex container" without having to remember the
25
+ * `width:'100%' / height:'100%' / resizeMode:'cover'` boilerplate.
26
+ * - Centralizes URL-vs-`{uri}` normalization that several components were
27
+ * re-implementing.
28
+ * - Uses the same `imageSource` prop name as the rest of the library
29
+ * (`Avatar`, `ProductLabel`, `CardProviderInfo`, ...) for a unified API.
30
+ *
31
+ * Layout rules:
32
+ * - If `ratio` is provided, the image lays out with `aspectRatio: ratio`
33
+ * and (unless `width` is given) fills the parent's width.
34
+ * - If neither `ratio` nor explicit dimensions are given, the image fills
35
+ * its parent (`width: '100%'`, `height: '100%'`) — same default as the
36
+ * most common usage in this library (background media, hero images).
37
+ */
38
+ function Image({
39
+ imageSource,
40
+ ratio,
41
+ resizeMode = 'cover',
42
+ width,
43
+ height,
44
+ borderRadius,
45
+ style,
46
+ accessibilityLabel,
47
+ accessibilityElementsHidden,
48
+ importantForAccessibility
49
+ }) {
50
+ const source = (0, _react.useMemo)(() => normalizeSource(imageSource), [imageSource]);
51
+ const layoutStyle = (0, _react.useMemo)(() => {
52
+ const s = {};
53
+ if (ratio != null) {
54
+ s.aspectRatio = ratio;
55
+ s.width = width ?? '100%';
56
+ if (height != null) s.height = height;
57
+ } else {
58
+ s.width = width ?? '100%';
59
+ s.height = height ?? '100%';
60
+ }
61
+ if (borderRadius != null) s.borderRadius = borderRadius;
62
+ return s;
63
+ }, [ratio, width, height, borderRadius]);
64
+ if (!source) {
65
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
66
+ style: [layoutStyle, style]
67
+ });
68
+ }
69
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
70
+ source: source,
71
+ style: [layoutStyle, style],
72
+ resizeMode: resizeMode,
73
+ accessibilityLabel: accessibilityLabel,
74
+ accessibilityElementsHidden: accessibilityElementsHidden,
75
+ importantForAccessibility: importantForAccessibility
76
+ });
77
+ }
78
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(Image);
@@ -13,56 +13,59 @@ exports.default = void 0;
13
13
  var _react = _interopRequireWildcard(require("react"));
14
14
  var _reactNative = require("react-native");
15
15
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
16
+ var _Image = _interopRequireDefault(require("../Image/Image"));
16
17
  var _reactUtils = require("../../utils/react-utils");
17
18
  var _jsxRuntime = require("react/jsx-runtime");
19
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
20
  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); }
19
- /**
20
- * Context to share 'modes' with child components.
21
- */const MediaCardContext = /*#__PURE__*/(0, _react.createContext)({});
21
+ const MediaCardContext = /*#__PURE__*/(0, _react.createContext)({});
22
22
  /**
23
23
  * MediaCard component implementation from Figma node 1241:4140.
24
- *
24
+ *
25
25
  * Features a background media slot, a large title, and a glass-morphism footer.
26
+ *
27
+ * The background can be supplied either as `imageSource` (preferred — uses
28
+ * the shared `<Image>` primitive under the hood) or as a custom `media` node
29
+ * for non-image backgrounds.
26
30
  */
27
31
  function MediaCard({
32
+ imageSource,
33
+ ratio,
28
34
  media,
29
35
  children,
30
36
  modes = _reactUtils.EMPTY_MODES,
31
37
  style
32
38
  }) {
33
- // Container Tokens
34
39
  const radius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/radius', modes) || '24');
35
40
  const gap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/gap', modes) || '0');
36
- // Dimensions from Figma: w=369, h=308. We can make it flexible or default to these?
37
- // Usually components should be flexible, but stories will constrain them.
38
- // Figma context shows fixed/hug behavior. Let's start with flex container.
39
-
40
41
  const containerStyle = {
41
42
  borderRadius: radius,
42
43
  gap,
43
44
  overflow: 'hidden',
44
45
  position: 'relative',
45
- // Default dimensions from Figma if needed, but better to let parent control or use defaults in stories.
46
- // However, to match "Maximize existing component usage", we follow patterns.
47
- // We'll trust the parent layout or style prop for width/height.
48
- minHeight: 308 // inferred from Figma height as a good default or minimum
46
+ minHeight: 308
49
47
  };
50
- const mediaWithModes = /*#__PURE__*/(0, _react.isValidElement)(media) ? /*#__PURE__*/(0, _react.cloneElement)(media, {
51
- modes: {
52
- ...media.props.modes,
53
- ...modes
54
- }
55
- }) : media;
48
+
49
+ // `media` wins for back-compat / custom nodes; otherwise we delegate to
50
+ // the shared <Image> for image-source backgrounds. All raster-rendering
51
+ // concerns (URL-vs-{uri}, resizeMode, aspect-ratio) live in <Image>.
52
+ const background = media ?? (imageSource != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
53
+ imageSource: imageSource,
54
+ ratio: ratio,
55
+ resizeMode: "cover",
56
+ accessibilityElementsHidden: true,
57
+ importantForAccessibility: "no"
58
+ }) : null);
56
59
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(MediaCardContext.Provider, {
57
60
  value: {
58
61
  modes
59
62
  },
60
63
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
61
64
  style: [containerStyle, style],
62
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
65
+ children: [background ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
63
66
  style: _reactNative.StyleSheet.absoluteFill,
64
- children: mediaWithModes
65
- }), children]
67
+ children: background
68
+ }) : null, children]
66
69
  })
67
70
  });
68
71
  }
@@ -80,10 +83,24 @@ function Header({
80
83
  children,
81
84
  style
82
85
  }) {
86
+ // NOTE: the previous `flex: 1` shorthand expanded on Yoga (Android) to
87
+ // `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }`. With `flexBasis: 0` the
88
+ // Header has *no intrinsic floor*, so when MediaCard is placed inside a
89
+ // height-unbounded parent — e.g. a Carousel slot whose contentContainer
90
+ // is `alignItems: 'flex-start'` — Yoga's first measurement pass sizes
91
+ // the Header at 0 and the card's overall height becomes non-deterministic.
92
+ // On native this manifests as the card "over-stretching" vertically (the
93
+ // same Yoga foot-gun we fixed in `CardCTA` rightWrap). Web hides it
94
+ // because browsers honor `min-height: auto` on flex items. Use explicit
95
+ // `flexGrow / flexShrink: 0 / flexBasis: 'auto'` so the Header is sized
96
+ // to its content as a floor and only grows to consume the extra space
97
+ // contributed by `MediaCard`'s `minHeight: 308`.
83
98
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
84
99
  style: [{
85
100
  padding: 16,
86
- flex: 1
101
+ flexGrow: 1,
102
+ flexShrink: 0,
103
+ flexBasis: 'auto'
87
104
  }, style],
88
105
  children: children
89
106
  });
@@ -129,8 +146,6 @@ function Footer({
129
146
  }) {
130
147
  const context = (0, _react.useContext)(MediaCardContext);
131
148
  const modes = propModes || context.modes || {};
132
-
133
- // Tokens
134
149
  const gap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/gap', modes) || '24');
135
150
  const paddingHorizontal = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/horizontal', modes) || '16');
136
151
  const paddingVertical = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/vertical', modes) || '12');
@@ -215,8 +230,6 @@ function FooterSubtitle({
215
230
  children: children
216
231
  });
217
232
  }
218
-
219
- // Attach sub-components
220
233
  MediaCard.Header = Header;
221
234
  MediaCard.Title = Title;
222
235
  MediaCard.Footer = Footer;
@@ -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) springs
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.withSpring)(h, {
188
- damping: 22,
189
- stiffness: 180,
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.springify().damping(18).delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS).reduceMotion(_reactNativeReanimated.ReduceMotion.System);
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,
@@ -201,6 +201,12 @@ Object.defineProperty(exports, "IconCapsule", {
201
201
  return _IconCapsule.default;
202
202
  }
203
203
  });
204
+ Object.defineProperty(exports, "Image", {
205
+ enumerable: true,
206
+ get: function () {
207
+ return _Image.default;
208
+ }
209
+ });
204
210
  Object.defineProperty(exports, "InputSearch", {
205
211
  enumerable: true,
206
212
  get: function () {
@@ -531,6 +537,7 @@ var _HoldingsCard = _interopRequireDefault(require("./HoldingsCard/HoldingsCard"
531
537
  var _HStack = _interopRequireDefault(require("./HStack/HStack"));
532
538
  var _IconButton = _interopRequireDefault(require("./IconButton/IconButton"));
533
539
  var _IconCapsule = _interopRequireDefault(require("./IconCapsule/IconCapsule"));
540
+ var _Image = _interopRequireDefault(require("./Image/Image"));
534
541
  var _LazyList = _interopRequireDefault(require("./LazyList/LazyList"));
535
542
  var _LinearMeter = _interopRequireDefault(require("./LinearMeter/LinearMeter"));
536
543
  var _ListGroup = _interopRequireDefault(require("./ListGroup/ListGroup"));