jfs-components 0.1.56 → 0.1.58

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,15 @@ 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.58] - 2026-07-27
8
+
9
+ - `TextInput` / `InputSearch` — restore iOS single-line vertical centering (FormField pattern): row owns height via `minHeight` + `paddingVertical: 0`; omit `lineHeight` on the field on iOS so placeholder and typed text stay centered (RN #39145 / #56774).
10
+
11
+ ## [0.1.57] - 2026-07-27
12
+
13
+ - `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.
14
+ - `CardCTA` (rating) — pin the footer when given an explicit height without `flexGrow` (which also caused infinite height in horizontal carousels).
15
+
7
16
  ## [0.1.56] - 2026-07-27
8
17
 
9
18
  - `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
@@ -149,6 +149,14 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)(function TextInput({
149
149
 
150
150
  // Convert radius to React Native format (if 99999, use a large value for fully rounded)
151
151
  const borderRadius = radius === 99999 ? 99999 : radius;
152
+
153
+ // Row height from tokens: vertical padding above/below the text line.
154
+ // Applied as container `minHeight` (FormField pattern) — NOT as TextInput
155
+ // `lineHeight` / `minHeight`, which breaks iOS vertical centering.
156
+ const padV = typeof paddingVertical === 'number' ? paddingVertical : 0;
157
+ const textLine = typeof lineHeight === 'number' ? lineHeight : 0;
158
+ const iconH = typeof iconSize === 'number' ? iconSize : 0;
159
+ const rowMinHeight = padV * 2 + Math.max(textLine, iconH);
152
160
  const containerStyle = {
153
161
  flexDirection: 'row',
154
162
  alignItems: 'center',
@@ -158,7 +166,10 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)(function TextInput({
158
166
  borderStyle: 'solid',
159
167
  borderRadius,
160
168
  paddingHorizontal,
161
- paddingVertical,
169
+ // Same as FormField / SuggestiveSearch: the ROW owns height via minHeight;
170
+ // paddingVertical on the row would fight that and let the field stretch.
171
+ paddingVertical: 0,
172
+ minHeight: rowMinHeight > 0 ? rowMinHeight : undefined,
162
173
  gap: 8
163
174
  };
164
175
  const hoverStyle = isHovered ? {
@@ -169,18 +180,15 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)(function TextInput({
169
180
  borderWidth: 1
170
181
  } : {};
171
182
 
172
- // ⚠️ DO NOT change to an unconditional `lineHeight`, and do NOT put
173
- // `minHeight: lineHeight` on this single-line input on iOS. Same contract as
174
- // FormField / SuggestiveSearch — read before "simplifying".
183
+ // ⚠️ Same contract as FormField / SuggestiveSearch do not "simplify".
175
184
  //
176
- // facebook/react-native#39145 / #28012: on iOS, a single-line RN TextInput
177
- // (UITextField) with `lineHeight` > font metrics puts ALL the extra space
178
- // ABOVE the glyphs, so placeholder/value sink. Android/Web distribute evenly.
179
- // RN's documented workaround (#39145) and #56774: omit `lineHeight` on
180
- // single-line iOS inputs UITextField centers natively — and let the
181
- // wrapper (`alignItems: 'center'` + padding/icon) own vertical layout.
182
- // Pinning `lineHeight` to `fontSize` is NOT a substitute; it clips custom
183
- // font metrics and floats typed text above center.
185
+ // facebook/react-native#39145 / #28012 / #56774:
186
+ // Single-line iOS UITextField + `lineHeight` (or a tall field box) puts ALL
187
+ // extra space ABOVE the glyphs placeholder/value sink. Documented fix:
188
+ // omit `lineHeight` on the single-line input; let the wrapper
189
+ // (`alignItems: 'center'` + `minHeight`) center it. UITextField centers
190
+ // natively when it is only as tall as its font. Do NOT pin lineHeight to
191
+ // fontSize (clips custom fonts / floats typed text).
184
192
  const textInputStyle = {
185
193
  flex: 1,
186
194
  color: textColor,