jfs-components 0.1.56 → 0.1.57

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,11 @@ 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.1.57] - 2026-07-27
8
+
9
+ - `Carousel` — fix infinite-tall slides: equal-height no longer uses unbounded `alignItems:'stretch'` / `height:'100%'` in a horizontal ScrollView; it measures intrinsic slide heights then locks slots to the tallest.
10
+ - `CardCTA` (rating) — pin the footer when given an explicit height without `flexGrow` (which also caused infinite height in horizontal carousels).
11
+
7
12
  ## [0.1.56] - 2026-07-27
8
13
 
9
14
  - `Carousel` — `equalHeight` defaults to `true` (hug tallest slide; no token `maxHeight` clip). New `itemInset`, `bleedHorizontal`, and `maxHeight` (`number | false`) props for edge-aligned cards without consumer math. Opt out with `equalHeight={false}` to keep the legacy clipped strip. Storybook docs refreshed for the new API.
@@ -215,12 +215,11 @@ function CardCTA({
215
215
  borderColor,
216
216
  flexDirection: isRating || isSip ? 'column' : 'row',
217
217
  overflow: 'visible',
218
- // Rating cards fill equal-height carousel slots and pin the footer down
219
- // when free space exists; intrinsic height is unchanged when not stretched.
218
+ // Pin footer to the bottom when a parent (e.g. equal-height Carousel)
219
+ // assigns an explicit height. Do not flexGrow that unbounded-grows
220
+ // inside horizontal ScrollViews and blows cards to infinite height.
220
221
  ...(isRating ? {
221
- alignSelf: 'stretch',
222
- justifyContent: 'space-between',
223
- flexGrow: 1
222
+ justifyContent: 'space-between'
224
223
  } : {})
225
224
  };
226
225
 
@@ -105,6 +105,35 @@ function Carousel({
105
105
  return containerWidth - containerPaddingH * 4;
106
106
  }, [itemWidthProp, itemInset, containerWidth, containerPaddingH, isNumbered]);
107
107
 
108
+ // Equal-height: measure each slide's intrinsic height, then lock every slot
109
+ // to the tallest. Do NOT use contentContainerStyle.alignItems:'stretch' or
110
+ // height:'100%' without a bounded cross-axis — in a horizontal ScrollView
111
+ // that resolves to an unbounded/infinite height.
112
+ const slideHeightsRef = (0, _react.useRef)({});
113
+ const [equalizedHeight, setEqualizedHeight] = (0, _react.useState)();
114
+ const equalizedHeightRef = (0, _react.useRef)(undefined);
115
+ equalizedHeightRef.current = equalizedHeight;
116
+ (0, _react.useEffect)(() => {
117
+ slideHeightsRef.current = {};
118
+ equalizedHeightRef.current = undefined;
119
+ setEqualizedHeight(undefined);
120
+ }, [totalItems, effectiveItemWidth, equalHeight]);
121
+ const handleSlideLayout = (0, _react.useCallback)(index => event => {
122
+ if (!equalHeight) return;
123
+ // Only measure while unconstrained. After lock, the slot height is forced
124
+ // and onLayout would just echo equalizedHeight.
125
+ if (equalizedHeightRef.current != null) return;
126
+ const height = Math.ceil(event.nativeEvent.layout.height);
127
+ if (height <= 0) return;
128
+ const previous = slideHeightsRef.current[index] ?? 0;
129
+ if (height <= previous) return;
130
+ slideHeightsRef.current[index] = height;
131
+ const measured = items.map((_, i) => slideHeightsRef.current[i]);
132
+ if (measured.some(value => value == null)) return;
133
+ const next = Math.max(...measured);
134
+ setEqualizedHeight(next);
135
+ }, [equalHeight, items]);
136
+
108
137
  // Snap interval = item width + gap
109
138
  const snapInterval = effectiveItemWidth + gap;
110
139
 
@@ -259,8 +288,9 @@ function Carousel({
259
288
  const contentContainerStyle = {
260
289
  paddingHorizontal: effectivePaddingH,
261
290
  gap,
262
- // Stretch so every slide matches the tallest (default). Opt out with equalHeight={false}.
263
- alignItems: equalHeight ? 'stretch' : 'flex-start'
291
+ // Always flex-start on the cross axis. Stretch + unbounded ScrollView
292
+ // height is what caused infinite-tall slides.
293
+ alignItems: 'flex-start'
264
294
  };
265
295
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(CarouselContext.Provider, {
266
296
  value: contextValue,
@@ -282,6 +312,8 @@ function Carousel({
282
312
  onScrollEndDrag: handleScrollEndDrag,
283
313
  onMomentumScrollEnd: finishProgrammaticScroll,
284
314
  children: items.map((child, index) => {
315
+ const hasEqualizedHeight = equalHeight && equalizedHeight != null && equalizedHeight > 0;
316
+
285
317
  // Strict slot box: width must be honored; never grow or shrink with
286
318
  // content, and clip anything that misbehaves (e.g. a child whose
287
319
  // inner flex layout would otherwise leak into the next slot on
@@ -292,8 +324,8 @@ function Carousel({
292
324
  flexShrink: 0,
293
325
  flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
294
326
  overflow: 'hidden',
295
- ...(equalHeight ? {
296
- alignSelf: 'stretch'
327
+ ...(hasEqualizedHeight ? {
328
+ height: equalizedHeight
297
329
  } : {})
298
330
  };
299
331
 
@@ -303,11 +335,11 @@ function Carousel({
303
335
  const childOverrideStyle = {
304
336
  width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
305
337
  maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
306
- flexGrow: equalHeight ? 1 : 0,
338
+ flexGrow: 0,
307
339
  flexShrink: 0,
308
- ...(equalHeight ? {
309
- alignSelf: 'stretch',
310
- height: '100%'
340
+ ...(hasEqualizedHeight ? {
341
+ height: '100%',
342
+ alignSelf: 'stretch'
311
343
  } : {})
312
344
  };
313
345
 
@@ -321,7 +353,11 @@ function Carousel({
321
353
  }) : child;
322
354
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
323
355
  style: slotStyle,
324
- children: childWithModes
356
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
357
+ style: hasEqualizedHeight ? styles.equalizedFill : undefined,
358
+ onLayout: handleSlideLayout(index),
359
+ children: childWithModes
360
+ })
325
361
  }, index);
326
362
  })
327
363
  }), !isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
@@ -470,6 +506,11 @@ function AnimatedDot({
470
506
  }
471
507
  });
472
508
  }
509
+ const styles = _reactNative.StyleSheet.create({
510
+ equalizedFill: {
511
+ flex: 1
512
+ }
513
+ });
473
514
 
474
515
  // ---------------------------------------------------------------------------
475
516
  // Attach sub-components