jfs-components 0.1.55 → 0.1.56

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.
@@ -58,8 +58,11 @@ export function cloneChildrenWithModes(children, modes, forcedModes) {
58
58
  } : modes;
59
59
  let processedChildren;
60
60
  if (hasChildren) {
61
+ // Descendants inherit this child's merged modes, not the ancestor's
62
+ // raw modes — so nested containers (e.g. ButtonGroup Neutral inside
63
+ // a CardCTA slot that injects Secondary) can cascade correctly.
61
64
  const childArray = React.Children.toArray(childChildren);
62
- const processed = cloneChildrenWithModes(childArray, modes, forcedModes);
65
+ const processed = cloneChildrenWithModes(childArray, mergedModes, forcedModes);
63
66
  processedChildren = processed.length === 1 ? processed[0] : processed;
64
67
  }
65
68
  result.push(/*#__PURE__*/React.cloneElement(child, {
@@ -9,7 +9,7 @@ export type ButtonGroupProps = {
9
9
  children?: React.ReactNode;
10
10
  /**
11
11
  * Mode configuration for design tokens (e.g., {"Button / Size": "M", "Emphasis": "High"})
12
- * These modes are passed down to all child components.
12
+ * These modes cascade to all children; a child's own `modes` still win on conflict.
13
13
  */
14
14
  modes?: Modes;
15
15
  /**
@@ -21,14 +21,14 @@ export type ButtonGroupProps = {
21
21
  * ButtonGroup component that aggregates multiple buttons (e.g., IconButton)
22
22
  * and handles their layout and styling/theming via design tokens.
23
23
  *
24
- * It passes the provided `modes` to all its immediate React Element children.
25
- * Non-IconButton children (e.g., Button) are automatically stretched to fill
26
- * available space. IconButton children keep their natural width.
24
+ * Group `modes` cascade to every child (and nested slot children). A child's
25
+ * own `modes` still win on conflict. Non-IconButton children (e.g. Button) are
26
+ * stretched to fill available space; IconButton children keep intrinsic width.
27
27
  *
28
28
  * @component
29
29
  * @example
30
30
  * ```jsx
31
- * <ButtonGroup modes={{"Color Mode": "Light"}}>
31
+ * <ButtonGroup modes={{"Color Mode": "Light", AppearanceBrand: "Neutral"}}>
32
32
  * <IconButton iconName="ic_qr_code" />
33
33
  * <Button label="Pay" />
34
34
  * </ButtonGroup>
@@ -31,6 +31,19 @@ export type CardCTAProps = {
31
31
  ratingLabel?: string;
32
32
  /** Show like/dislike actions in the rating footer */
33
33
  showRatingActions?: boolean;
34
+ /**
35
+ * [rating] Show the primary footer button (default `Save` / `buttonSlot`).
36
+ * When `false`, only rating actions render and they stay start-aligned —
37
+ * no empty button slot and no consumer `buttonSlot={<Fragment />}` hack.
38
+ * Default: `true`.
39
+ */
40
+ showButton?: boolean;
41
+ /**
42
+ * [rating] Horizontal alignment of the media / badge slot above the title.
43
+ * Charts and dataviz should use `'center'` (default). Use `'start'` for
44
+ * left-aligned badges/labels.
45
+ */
46
+ ratingMediaAlign?: 'start' | 'center';
34
47
  /** Callback for the default like action */
35
48
  onPressLike?: () => void;
36
49
  /** Callback for the default dislike action */
@@ -58,6 +71,6 @@ export type CardCTAProps = {
58
71
  /** Container style overrides */
59
72
  style?: StyleProp<ViewStyle>;
60
73
  };
61
- declare function CardCTA({ type, title, body, iconName, buttonLabel, onPressButton, ratingLabel, showRatingActions, onPressLike, onPressDislike, headline, description, subtitle, avatarSource, modes: propModes, iconSlot, buttonSlot, ratingBadgeSlot, ratingActionsSlot, avatarSlot, style, }: CardCTAProps): import("react/jsx-runtime").JSX.Element;
74
+ declare function CardCTA({ type, title, body, iconName, buttonLabel, onPressButton, ratingLabel, showRatingActions, showButton, ratingMediaAlign, onPressLike, onPressDislike, headline, description, subtitle, avatarSource, modes: propModes, iconSlot, buttonSlot, ratingBadgeSlot, ratingActionsSlot, avatarSlot, style, }: CardCTAProps): import("react/jsx-runtime").JSX.Element;
62
75
  export default CardCTA;
63
76
  //# sourceMappingURL=CardCTA.d.ts.map
@@ -18,16 +18,44 @@ export interface CarouselProps {
18
18
  gap?: number;
19
19
  /** Width of each carousel item. When undefined, items take full container width. */
20
20
  itemWidth?: number;
21
+ /**
22
+ * Inset from each horizontal edge of the carousel to the active item.
23
+ * Sets scroll `paddingHorizontal` to this value and, when `itemWidth` is
24
+ * omitted, derives `itemWidth` as `containerWidth - itemInset * 2`.
25
+ * Prefer this over manually pairing `itemWidth` + `paddingHorizontal`.
26
+ */
27
+ itemInset?: number;
28
+ /**
29
+ * Negative horizontal margin (px) so the carousel can bleed past a padded
30
+ * parent (e.g. Section / VStack). Combine with `itemInset` for
31
+ * screen-edge-aligned cards without consumer math.
32
+ */
33
+ bleedHorizontal?: number;
21
34
  /**
22
35
  * Explicit horizontal padding (px) for the scroll content. When provided it
23
36
  * overrides the token-derived padding verbatim — the built-in `carousel/
24
37
  * padding/horizontal × 2` "peek" multiplier is NOT applied. Use this for
25
38
  * presets (e.g. the account-card strip) whose own tokens define a flush,
26
- * non-peeking layout.
39
+ * non-peeking layout. Ignored when `itemInset` is set.
27
40
  */
28
41
  paddingHorizontal?: number;
29
42
  /** Explicit vertical padding (px) for the outer container. Overrides `carousel/padding/vertical`. */
30
43
  paddingVertical?: number;
44
+ /**
45
+ * When true (default), every slide stretches to the tallest slide's height
46
+ * so mixed-content cards share one row height. Set `false` to keep each
47
+ * slide at its intrinsic height.
48
+ */
49
+ equalHeight?: boolean;
50
+ /**
51
+ * Hard max height for the carousel shell.
52
+ * - `undefined` (default): apply the `carousel/maxHeight` token only when
53
+ * `equalHeight` is `false` (legacy clipped strip). With `equalHeight`
54
+ * (default) the shell hugs content and is not clipped.
55
+ * - `number`: always apply that max height.
56
+ * - `false`: never apply a max height.
57
+ */
58
+ maxHeight?: number | false;
31
59
  /**
32
60
  * Carousel visual type.
33
61
  * - `'Default'` — standard carousel with dot pagination below.
@@ -40,7 +68,7 @@ export interface CarouselProps {
40
68
  /** Style overrides for the outermost container. */
41
69
  style?: StyleProp<ViewStyle>;
42
70
  }
43
- export declare function Carousel({ children, modes, autoPlay, autoPlayInterval, showPagination, loop, gap: gapProp, itemWidth: itemWidthProp, paddingHorizontal: paddingHorizontalProp, paddingVertical: paddingVerticalProp, type, onIndexChange, style, }: CarouselProps): import("react/jsx-runtime").JSX.Element;
71
+ export declare function Carousel({ children, modes, autoPlay, autoPlayInterval, showPagination, loop, gap: gapProp, itemWidth: itemWidthProp, itemInset, bleedHorizontal, paddingHorizontal: paddingHorizontalProp, paddingVertical: paddingVerticalProp, equalHeight, maxHeight: maxHeightProp, type, onIndexChange, style, }: CarouselProps): import("react/jsx-runtime").JSX.Element;
44
72
  export declare namespace Carousel {
45
73
  var Item: typeof import("./Carousel").Item;
46
74
  var Pagination: typeof import("./Carousel").Pagination;
@@ -179,6 +179,7 @@ export declare const FIGMA_MODES: {
179
179
  readonly "NavArrow Direction": readonly ["Left&Right", "Top&Bottom"];
180
180
  readonly "NoteInput / Output": readonly ["Default"];
181
181
  readonly "Nudge / Output": readonly ["Default"];
182
+ readonly "Nudge Border Radius ": readonly ["Default", "Pill"];
182
183
  readonly "Nudge padding": readonly ["Default", "None"];
183
184
  readonly "number pagination": readonly ["Default"];
184
185
  readonly "Numpad / Output": readonly ["Default"];
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-07-27T16:45:55.755Z
7
+ * Generated: 2026-07-27T20:32:46.426Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -5,7 +5,11 @@ import {
5
5
  type ViewStyle,
6
6
  } from 'react-native'
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
8
- import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils'
8
+ import {
9
+ EMPTY_MODES,
10
+ cloneChildrenWithModes,
11
+ flattenChildren,
12
+ } from '../../utils/react-utils'
9
13
  import IconButton from '../IconButton/IconButton'
10
14
  import type { Modes } from '../../design-tokens'
11
15
 
@@ -17,7 +21,7 @@ export type ButtonGroupProps = {
17
21
  children?: React.ReactNode;
18
22
  /**
19
23
  * Mode configuration for design tokens (e.g., {"Button / Size": "M", "Emphasis": "High"})
20
- * These modes are passed down to all child components.
24
+ * These modes cascade to all children; a child's own `modes` still win on conflict.
21
25
  */
22
26
  modes?: Modes;
23
27
  /**
@@ -30,14 +34,14 @@ export type ButtonGroupProps = {
30
34
  * ButtonGroup component that aggregates multiple buttons (e.g., IconButton)
31
35
  * and handles their layout and styling/theming via design tokens.
32
36
  *
33
- * It passes the provided `modes` to all its immediate React Element children.
34
- * Non-IconButton children (e.g., Button) are automatically stretched to fill
35
- * available space. IconButton children keep their natural width.
37
+ * Group `modes` cascade to every child (and nested slot children). A child's
38
+ * own `modes` still win on conflict. Non-IconButton children (e.g. Button) are
39
+ * stretched to fill available space; IconButton children keep intrinsic width.
36
40
  *
37
41
  * @component
38
42
  * @example
39
43
  * ```jsx
40
- * <ButtonGroup modes={{"Color Mode": "Light"}}>
44
+ * <ButtonGroup modes={{"Color Mode": "Light", AppearanceBrand: "Neutral"}}>
41
45
  * <IconButton iconName="ic_qr_code" />
42
46
  * <Button label="Pay" />
43
47
  * </ButtonGroup>
@@ -48,12 +52,10 @@ function ButtonGroup({
48
52
  modes = EMPTY_MODES,
49
53
  style,
50
54
  }: ButtonGroupProps) {
51
- // Resolve design tokens
52
55
  const gap = getVariableByName('buttonGroup/gap', modes) ?? 12
53
56
  const paddingHorizontal = getVariableByName('buttonGroup/padding/horizontal', modes) ?? 0
54
57
  const paddingVertical = getVariableByName('buttonGroup/padding/vertical', modes) ?? 0
55
58
 
56
- // Container style
57
59
  const containerStyle: ViewStyle = {
58
60
  flexDirection: 'row',
59
61
  alignItems: 'center',
@@ -62,25 +64,21 @@ function ButtonGroup({
62
64
  paddingVertical: paddingVertical,
63
65
  }
64
66
 
65
- // Flatten children to handle Fragments properly
66
67
  const flatChildren = flattenChildren(children)
68
+ const cascadedChildren = cloneChildrenWithModes(flatChildren, modes)
67
69
 
68
- const childrenWithModes = React.Children.map(flatChildren, (child) => {
69
- if (React.isValidElement(child)) {
70
- const element = child as React.ReactElement<any>
71
- const childModes = element.props.modes || {}
72
- const mergedModes = { ...modes, ...childModes }
73
- const isIconButton = element.type === IconButton
74
- const stretchStyle = isIconButton
75
- ? undefined
76
- : { flex: 1 }
77
-
78
- return React.cloneElement(element, {
79
- modes: mergedModes,
80
- style: [stretchStyle, element.props.style],
81
- })
70
+ const childrenWithModes = React.Children.map(cascadedChildren, (child) => {
71
+ if (!React.isValidElement(child)) {
72
+ return child
82
73
  }
83
- return child
74
+
75
+ const element = child as React.ReactElement<any>
76
+ const isIconButton = element.type === IconButton
77
+ const stretchStyle = isIconButton ? undefined : { flex: 1 }
78
+
79
+ return React.cloneElement(element, {
80
+ style: [stretchStyle, element.props.style],
81
+ })
84
82
  })
85
83
 
86
84
  return (
@@ -63,6 +63,19 @@ export type CardCTAProps = {
63
63
  ratingLabel?: string;
64
64
  /** Show like/dislike actions in the rating footer */
65
65
  showRatingActions?: boolean;
66
+ /**
67
+ * [rating] Show the primary footer button (default `Save` / `buttonSlot`).
68
+ * When `false`, only rating actions render and they stay start-aligned —
69
+ * no empty button slot and no consumer `buttonSlot={<Fragment />}` hack.
70
+ * Default: `true`.
71
+ */
72
+ showButton?: boolean;
73
+ /**
74
+ * [rating] Horizontal alignment of the media / badge slot above the title.
75
+ * Charts and dataviz should use `'center'` (default). Use `'start'` for
76
+ * left-aligned badges/labels.
77
+ */
78
+ ratingMediaAlign?: 'start' | 'center';
66
79
  /** Callback for the default like action */
67
80
  onPressLike?: () => void;
68
81
  /** Callback for the default dislike action */
@@ -100,6 +113,8 @@ function CardCTA({
100
113
  onPressButton,
101
114
  ratingLabel = '+28 Rating',
102
115
  showRatingActions = true,
116
+ showButton = true,
117
+ ratingMediaAlign = 'center',
103
118
  onPressLike,
104
119
  onPressDislike,
105
120
  headline = 'Headline',
@@ -187,7 +202,15 @@ function CardCTA({
187
202
  const avatarModes = { ...modes, 'Avatar Size': 'L' }
188
203
 
189
204
  const buttonModes = {...modes, AppearanceBrand: 'Secondary', 'Button / Size': 'S'}
190
- const iconButtonModes = {'Button / Size': 'S', 'Emphasis': 'Low', 'AppearanceBrand':'Neutral',...modes }
205
+ // Rating footer IconButtons are Neutral / S / Low per Figma. Apply those
206
+ // AFTER cascaded modes so card-level AppearanceBrand (often Secondary)
207
+ // cannot override the footer chrome.
208
+ const iconButtonModes: Modes = {
209
+ ...modes,
210
+ 'Button / Size': 'S',
211
+ Emphasis: 'Low',
212
+ AppearanceBrand: 'Neutral',
213
+ }
191
214
  const effectiveButtonLabel = buttonLabel ?? (isRating ? 'Save' : 'Button')
192
215
  const nonWrappingButtonLabel = effectiveButtonLabel.replace(/\s/g, '\u00A0')
193
216
  const [measuredButtonLabelWidth, setMeasuredButtonLabelWidth] = React.useState<number | null>(null)
@@ -261,6 +284,15 @@ function CardCTA({
261
284
  borderColor,
262
285
  flexDirection: isRating || isSip ? 'column' : 'row',
263
286
  overflow: 'visible',
287
+ // Rating cards fill equal-height carousel slots and pin the footer down
288
+ // when free space exists; intrinsic height is unchanged when not stretched.
289
+ ...(isRating
290
+ ? {
291
+ alignSelf: 'stretch',
292
+ justifyContent: 'space-between' as const,
293
+ flexGrow: 1,
294
+ }
295
+ : {}),
264
296
  }
265
297
 
266
298
  // NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
@@ -417,16 +449,24 @@ function CardCTA({
417
449
  paddingVertical: ratingContentPaddingV,
418
450
  gap: ratingContentGap,
419
451
  alignItems: 'flex-start',
452
+ alignSelf: 'stretch',
453
+ }
454
+
455
+ const ratingMediaStyle: ViewStyle = {
456
+ alignSelf: 'stretch',
457
+ width: '100%',
458
+ alignItems: ratingMediaAlign === 'center' ? 'center' : 'flex-start',
420
459
  }
421
460
 
422
461
  const ratingFooterStyle: ViewStyle = {
423
462
  flexDirection: 'row',
424
463
  alignItems: 'flex-start',
425
- justifyContent: 'space-between',
464
+ justifyContent: showButton ? 'space-between' : 'flex-start',
426
465
  paddingHorizontal: ratingFooterPaddingH,
427
466
  paddingTop: ratingFooterPaddingTop,
428
467
  paddingBottom: ratingFooterPaddingBottom,
429
468
  overflow: 'visible',
469
+ alignSelf: 'stretch',
430
470
  }
431
471
 
432
472
  const buttonLabelStyle: TextStyle = {
@@ -503,30 +543,34 @@ function CardCTA({
503
543
  return (
504
544
  <View style={[containerStyle, style]}>
505
545
  <View style={ratingContentStyle}>
506
- {ratingBadgeSlot ? (
507
- cloneChildrenWithModes(ratingBadgeSlot, modes)
508
- ) : (
509
- <Badge label={ratingLabel} modes={modes} />
510
- )}
546
+ <View style={ratingMediaStyle}>
547
+ {ratingBadgeSlot ? (
548
+ cloneChildrenWithModes(ratingBadgeSlot, modes)
549
+ ) : (
550
+ <Badge label={ratingLabel} modes={modes} />
551
+ )}
552
+ </View>
511
553
  <View style={textWrapStyle}>
512
554
  <Text style={titleStyle}>{title}</Text>
513
555
  <Text style={bodyStyle}>{body}</Text>
514
556
  </View>
515
557
  </View>
516
558
  <View style={ratingFooterStyle}>
517
- <View style={ratingButtonWrapStyle}>
518
- {buttonSlot ? (
519
- cloneChildrenWithModes(buttonSlot, buttonModes)
520
- ) : (
521
- <Button
522
- label={effectiveButtonLabel}
523
- onPress={onPressButton || (() => {})}
524
- modes={buttonModes}
525
- style={ratingButtonStyle}
526
- renderContent={renderButtonContent(ratingButtonLabelStyle)}
527
- />
528
- )}
529
- </View>
559
+ {showButton ? (
560
+ <View style={ratingButtonWrapStyle}>
561
+ {buttonSlot ? (
562
+ cloneChildrenWithModes(buttonSlot, buttonModes)
563
+ ) : (
564
+ <Button
565
+ label={effectiveButtonLabel}
566
+ onPress={onPressButton || (() => {})}
567
+ modes={buttonModes}
568
+ style={ratingButtonStyle}
569
+ renderContent={renderButtonContent(ratingButtonLabelStyle)}
570
+ />
571
+ )}
572
+ </View>
573
+ ) : null}
530
574
  {showRatingActions ? (
531
575
  ratingActionsSlot ? (
532
576
  cloneChildrenWithModes(ratingActionsSlot, iconButtonModes)
@@ -535,11 +579,13 @@ function CardCTA({
535
579
  <IconButton
536
580
  iconName="ic_like"
537
581
  accessibilityLabel="Like"
582
+ modes={iconButtonModes}
538
583
  {...(onPressLike ? { onPress: onPressLike } : {})}
539
584
  />
540
585
  <IconButton
541
586
  iconName="ic_dislike"
542
587
  accessibilityLabel="Dislike"
588
+ modes={iconButtonModes}
543
589
  {...(onPressDislike ? { onPress: onPressDislike } : {})}
544
590
  />
545
591
  </ButtonGroup>
@@ -67,16 +67,44 @@ export interface CarouselProps {
67
67
  gap?: number
68
68
  /** Width of each carousel item. When undefined, items take full container width. */
69
69
  itemWidth?: number
70
+ /**
71
+ * Inset from each horizontal edge of the carousel to the active item.
72
+ * Sets scroll `paddingHorizontal` to this value and, when `itemWidth` is
73
+ * omitted, derives `itemWidth` as `containerWidth - itemInset * 2`.
74
+ * Prefer this over manually pairing `itemWidth` + `paddingHorizontal`.
75
+ */
76
+ itemInset?: number
77
+ /**
78
+ * Negative horizontal margin (px) so the carousel can bleed past a padded
79
+ * parent (e.g. Section / VStack). Combine with `itemInset` for
80
+ * screen-edge-aligned cards without consumer math.
81
+ */
82
+ bleedHorizontal?: number
70
83
  /**
71
84
  * Explicit horizontal padding (px) for the scroll content. When provided it
72
85
  * overrides the token-derived padding verbatim — the built-in `carousel/
73
86
  * padding/horizontal × 2` "peek" multiplier is NOT applied. Use this for
74
87
  * presets (e.g. the account-card strip) whose own tokens define a flush,
75
- * non-peeking layout.
88
+ * non-peeking layout. Ignored when `itemInset` is set.
76
89
  */
77
90
  paddingHorizontal?: number
78
91
  /** Explicit vertical padding (px) for the outer container. Overrides `carousel/padding/vertical`. */
79
92
  paddingVertical?: number
93
+ /**
94
+ * When true (default), every slide stretches to the tallest slide's height
95
+ * so mixed-content cards share one row height. Set `false` to keep each
96
+ * slide at its intrinsic height.
97
+ */
98
+ equalHeight?: boolean
99
+ /**
100
+ * Hard max height for the carousel shell.
101
+ * - `undefined` (default): apply the `carousel/maxHeight` token only when
102
+ * `equalHeight` is `false` (legacy clipped strip). With `equalHeight`
103
+ * (default) the shell hugs content and is not clipped.
104
+ * - `number`: always apply that max height.
105
+ * - `false`: never apply a max height.
106
+ */
107
+ maxHeight?: number | false
80
108
  /**
81
109
  * Carousel visual type.
82
110
  * - `'Default'` — standard carousel with dot pagination below.
@@ -104,8 +132,12 @@ export function Carousel({
104
132
  loop = true,
105
133
  gap: gapProp,
106
134
  itemWidth: itemWidthProp,
135
+ itemInset,
136
+ bleedHorizontal,
107
137
  paddingHorizontal: paddingHorizontalProp,
108
138
  paddingVertical: paddingVerticalProp,
139
+ equalHeight = true,
140
+ maxHeight: maxHeightProp,
109
141
  type = 'Default',
110
142
 
111
143
  onIndexChange,
@@ -127,7 +159,7 @@ export function Carousel({
127
159
  getVariableByName('carousel/padding/vertical', modes),
128
160
  )
129
161
  // Outer container max height per Figma (`carousel/maxHeight`).
130
- const maxHeight = parseFloat(
162
+ const tokenMaxHeight = parseFloat(
131
163
  getVariableByName('carousel/maxHeight', modes),
132
164
  )
133
165
  // Gap between AutoplayControl and NumberPagination in the Numbered overlay.
@@ -165,15 +197,18 @@ export function Carousel({
165
197
 
166
198
  const totalItems = items.length
167
199
 
168
- // Effective item width: provided prop, or full container width minus peek offsets.
200
+ // Effective item width: explicit prop, itemInset derivation, or token peek.
169
201
  // For the Numbered variant the items are full-bleed (no peek padding).
170
202
  const effectiveItemWidth = useMemo(() => {
171
203
  if (itemWidthProp != null) return itemWidthProp
172
204
  if (containerWidth === 0) return 0
173
205
  if (isNumbered) return containerWidth
206
+ if (itemInset != null) {
207
+ return Math.max(0, containerWidth - itemInset * 2)
208
+ }
174
209
  // Full-width minus peekOffset on each side
175
210
  return containerWidth - containerPaddingH * 4
176
- }, [itemWidthProp, containerWidth, containerPaddingH, isNumbered])
211
+ }, [itemWidthProp, itemInset, containerWidth, containerPaddingH, isNumbered])
177
212
 
178
213
  // Snap interval = item width + gap
179
214
  const snapInterval = effectiveItemWidth + gap
@@ -345,15 +380,31 @@ export function Carousel({
345
380
  }, [isPlaying, startAutoPlay, clearAutoPlay])
346
381
 
347
382
  // ---- Render ----
348
- // Explicit padding props win verbatim; otherwise fall back to the
349
- // token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
350
- // Numbered variant is full-bleed: zero padding.
351
- const effectivePaddingH = isNumbered ? 0 : (paddingHorizontalProp ?? containerPaddingH)
383
+ // Padding priority: Numbered 0; itemInset inset; explicit padding →
384
+ // verbatim; else token (legacy ×2 peek is applied via item width, not here).
385
+ const effectivePaddingH = isNumbered
386
+ ? 0
387
+ : (itemInset ?? paddingHorizontalProp ?? containerPaddingH)
352
388
  const effectivePaddingV = isNumbered ? 0 : (paddingVerticalProp ?? containerPaddingV)
353
389
 
390
+ // Equal-height rows hug content — don't clip with the token maxHeight unless
391
+ // the caller opts into a numeric max (or disables equalHeight for legacy strips).
392
+ const resolvedMaxHeight = isNumbered
393
+ ? undefined
394
+ : maxHeightProp === false
395
+ ? undefined
396
+ : typeof maxHeightProp === 'number'
397
+ ? maxHeightProp
398
+ : equalHeight
399
+ ? undefined
400
+ : tokenMaxHeight
401
+
354
402
  const outerStyle: ViewStyle = {
355
403
  paddingVertical: effectivePaddingV,
356
- maxHeight: isNumbered ? undefined : maxHeight,
404
+ maxHeight: resolvedMaxHeight,
405
+ ...(bleedHorizontal != null
406
+ ? { marginHorizontal: -Math.abs(bleedHorizontal) }
407
+ : {}),
357
408
  // Numbered needs relative positioning for the floating controls overlay.
358
409
  ...(isNumbered ? { overflow: 'hidden' as const } : {}),
359
410
  }
@@ -361,8 +412,8 @@ export function Carousel({
361
412
  const contentContainerStyle: ViewStyle = {
362
413
  paddingHorizontal: effectivePaddingH,
363
414
  gap,
364
- // Align items to the start so snap works correctly
365
- alignItems: 'flex-start',
415
+ // Stretch so every slide matches the tallest (default). Opt out with equalHeight={false}.
416
+ alignItems: equalHeight ? 'stretch' : 'flex-start',
366
417
  }
367
418
 
368
419
  return (
@@ -394,6 +445,7 @@ export function Carousel({
394
445
  flexShrink: 0,
395
446
  flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
396
447
  overflow: 'hidden',
448
+ ...(equalHeight ? { alignSelf: 'stretch' } : {}),
397
449
  }
398
450
 
399
451
  // The cloned style forces the child's outer node to also honor the
@@ -402,8 +454,11 @@ export function Carousel({
402
454
  const childOverrideStyle: ViewStyle = {
403
455
  width: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
404
456
  maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
405
- flexGrow: 0,
457
+ flexGrow: equalHeight ? 1 : 0,
406
458
  flexShrink: 0,
459
+ ...(equalHeight
460
+ ? { alignSelf: 'stretch', height: '100%' as const }
461
+ : {}),
407
462
  }
408
463
 
409
464
  // Pass modes down to children