jfs-components 0.1.55 → 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 +10 -0
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +16 -26
- package/lib/commonjs/components/CardCTA/CardCTA.js +35 -12
- package/lib/commonjs/components/Carousel/Carousel.js +73 -12
- package/lib/commonjs/design-tokens/figma-modes.generated.js +10 -9
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/react-utils.js +4 -1
- package/lib/module/components/ButtonGroup/ButtonGroup.js +17 -27
- package/lib/module/components/CardCTA/CardCTA.js +35 -12
- package/lib/module/components/Carousel/Carousel.js +74 -13
- package/lib/module/design-tokens/figma-modes.generated.js +10 -9
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/react-utils.js +4 -1
- package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +5 -5
- package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +14 -1
- package/lib/typescript/src/components/Carousel/Carousel.d.ts +30 -2
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +1 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ButtonGroup/ButtonGroup.tsx +22 -24
- package/src/components/CardCTA/CardCTA.tsx +65 -20
- package/src/components/Carousel/Carousel.tsx +125 -11
- package/src/design-tokens/figma-modes.generated.ts +10 -9
- package/src/icons/registry.ts +1 -1
- package/src/utils/react-utils.ts +8 -1
|
@@ -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,
|
|
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
|
|
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
|
-
*
|
|
25
|
-
* Non-IconButton children (e.g
|
|
26
|
-
* available space
|
|
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-
|
|
7
|
+
* Generated: 2026-07-27T20:45:13.986Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
*
|
|
34
|
-
* Non-IconButton children (e.g
|
|
35
|
-
* available space
|
|
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(
|
|
69
|
-
if (React.isValidElement(child)) {
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,14 @@ function CardCTA({
|
|
|
261
284
|
borderColor,
|
|
262
285
|
flexDirection: isRating || isSip ? 'column' : 'row',
|
|
263
286
|
overflow: 'visible',
|
|
287
|
+
// Pin footer to the bottom when a parent (e.g. equal-height Carousel)
|
|
288
|
+
// assigns an explicit height. Do not flexGrow — that unbounded-grows
|
|
289
|
+
// inside horizontal ScrollViews and blows cards to infinite height.
|
|
290
|
+
...(isRating
|
|
291
|
+
? {
|
|
292
|
+
justifyContent: 'space-between' as const,
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
264
295
|
}
|
|
265
296
|
|
|
266
297
|
// NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
|
|
@@ -417,16 +448,24 @@ function CardCTA({
|
|
|
417
448
|
paddingVertical: ratingContentPaddingV,
|
|
418
449
|
gap: ratingContentGap,
|
|
419
450
|
alignItems: 'flex-start',
|
|
451
|
+
alignSelf: 'stretch',
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const ratingMediaStyle: ViewStyle = {
|
|
455
|
+
alignSelf: 'stretch',
|
|
456
|
+
width: '100%',
|
|
457
|
+
alignItems: ratingMediaAlign === 'center' ? 'center' : 'flex-start',
|
|
420
458
|
}
|
|
421
459
|
|
|
422
460
|
const ratingFooterStyle: ViewStyle = {
|
|
423
461
|
flexDirection: 'row',
|
|
424
462
|
alignItems: 'flex-start',
|
|
425
|
-
justifyContent: 'space-between',
|
|
463
|
+
justifyContent: showButton ? 'space-between' : 'flex-start',
|
|
426
464
|
paddingHorizontal: ratingFooterPaddingH,
|
|
427
465
|
paddingTop: ratingFooterPaddingTop,
|
|
428
466
|
paddingBottom: ratingFooterPaddingBottom,
|
|
429
467
|
overflow: 'visible',
|
|
468
|
+
alignSelf: 'stretch',
|
|
430
469
|
}
|
|
431
470
|
|
|
432
471
|
const buttonLabelStyle: TextStyle = {
|
|
@@ -503,30 +542,34 @@ function CardCTA({
|
|
|
503
542
|
return (
|
|
504
543
|
<View style={[containerStyle, style]}>
|
|
505
544
|
<View style={ratingContentStyle}>
|
|
506
|
-
{
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
545
|
+
<View style={ratingMediaStyle}>
|
|
546
|
+
{ratingBadgeSlot ? (
|
|
547
|
+
cloneChildrenWithModes(ratingBadgeSlot, modes)
|
|
548
|
+
) : (
|
|
549
|
+
<Badge label={ratingLabel} modes={modes} />
|
|
550
|
+
)}
|
|
551
|
+
</View>
|
|
511
552
|
<View style={textWrapStyle}>
|
|
512
553
|
<Text style={titleStyle}>{title}</Text>
|
|
513
554
|
<Text style={bodyStyle}>{body}</Text>
|
|
514
555
|
</View>
|
|
515
556
|
</View>
|
|
516
557
|
<View style={ratingFooterStyle}>
|
|
517
|
-
|
|
518
|
-
{
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
558
|
+
{showButton ? (
|
|
559
|
+
<View style={ratingButtonWrapStyle}>
|
|
560
|
+
{buttonSlot ? (
|
|
561
|
+
cloneChildrenWithModes(buttonSlot, buttonModes)
|
|
562
|
+
) : (
|
|
563
|
+
<Button
|
|
564
|
+
label={effectiveButtonLabel}
|
|
565
|
+
onPress={onPressButton || (() => {})}
|
|
566
|
+
modes={buttonModes}
|
|
567
|
+
style={ratingButtonStyle}
|
|
568
|
+
renderContent={renderButtonContent(ratingButtonLabelStyle)}
|
|
569
|
+
/>
|
|
570
|
+
)}
|
|
571
|
+
</View>
|
|
572
|
+
) : null}
|
|
530
573
|
{showRatingActions ? (
|
|
531
574
|
ratingActionsSlot ? (
|
|
532
575
|
cloneChildrenWithModes(ratingActionsSlot, iconButtonModes)
|
|
@@ -535,11 +578,13 @@ function CardCTA({
|
|
|
535
578
|
<IconButton
|
|
536
579
|
iconName="ic_like"
|
|
537
580
|
accessibilityLabel="Like"
|
|
581
|
+
modes={iconButtonModes}
|
|
538
582
|
{...(onPressLike ? { onPress: onPressLike } : {})}
|
|
539
583
|
/>
|
|
540
584
|
<IconButton
|
|
541
585
|
iconName="ic_dislike"
|
|
542
586
|
accessibilityLabel="Dislike"
|
|
587
|
+
modes={iconButtonModes}
|
|
543
588
|
{...(onPressDislike ? { onPress: onPressDislike } : {})}
|
|
544
589
|
/>
|
|
545
590
|
</ButtonGroup>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ScrollView,
|
|
13
13
|
Animated,
|
|
14
14
|
Pressable,
|
|
15
|
+
StyleSheet,
|
|
15
16
|
type ViewStyle,
|
|
16
17
|
type StyleProp,
|
|
17
18
|
type NativeSyntheticEvent,
|
|
@@ -67,16 +68,44 @@ export interface CarouselProps {
|
|
|
67
68
|
gap?: number
|
|
68
69
|
/** Width of each carousel item. When undefined, items take full container width. */
|
|
69
70
|
itemWidth?: number
|
|
71
|
+
/**
|
|
72
|
+
* Inset from each horizontal edge of the carousel to the active item.
|
|
73
|
+
* Sets scroll `paddingHorizontal` to this value and, when `itemWidth` is
|
|
74
|
+
* omitted, derives `itemWidth` as `containerWidth - itemInset * 2`.
|
|
75
|
+
* Prefer this over manually pairing `itemWidth` + `paddingHorizontal`.
|
|
76
|
+
*/
|
|
77
|
+
itemInset?: number
|
|
78
|
+
/**
|
|
79
|
+
* Negative horizontal margin (px) so the carousel can bleed past a padded
|
|
80
|
+
* parent (e.g. Section / VStack). Combine with `itemInset` for
|
|
81
|
+
* screen-edge-aligned cards without consumer math.
|
|
82
|
+
*/
|
|
83
|
+
bleedHorizontal?: number
|
|
70
84
|
/**
|
|
71
85
|
* Explicit horizontal padding (px) for the scroll content. When provided it
|
|
72
86
|
* overrides the token-derived padding verbatim — the built-in `carousel/
|
|
73
87
|
* padding/horizontal × 2` "peek" multiplier is NOT applied. Use this for
|
|
74
88
|
* presets (e.g. the account-card strip) whose own tokens define a flush,
|
|
75
|
-
* non-peeking layout.
|
|
89
|
+
* non-peeking layout. Ignored when `itemInset` is set.
|
|
76
90
|
*/
|
|
77
91
|
paddingHorizontal?: number
|
|
78
92
|
/** Explicit vertical padding (px) for the outer container. Overrides `carousel/padding/vertical`. */
|
|
79
93
|
paddingVertical?: number
|
|
94
|
+
/**
|
|
95
|
+
* When true (default), every slide stretches to the tallest slide's height
|
|
96
|
+
* so mixed-content cards share one row height. Set `false` to keep each
|
|
97
|
+
* slide at its intrinsic height.
|
|
98
|
+
*/
|
|
99
|
+
equalHeight?: boolean
|
|
100
|
+
/**
|
|
101
|
+
* Hard max height for the carousel shell.
|
|
102
|
+
* - `undefined` (default): apply the `carousel/maxHeight` token only when
|
|
103
|
+
* `equalHeight` is `false` (legacy clipped strip). With `equalHeight`
|
|
104
|
+
* (default) the shell hugs content and is not clipped.
|
|
105
|
+
* - `number`: always apply that max height.
|
|
106
|
+
* - `false`: never apply a max height.
|
|
107
|
+
*/
|
|
108
|
+
maxHeight?: number | false
|
|
80
109
|
/**
|
|
81
110
|
* Carousel visual type.
|
|
82
111
|
* - `'Default'` — standard carousel with dot pagination below.
|
|
@@ -104,8 +133,12 @@ export function Carousel({
|
|
|
104
133
|
loop = true,
|
|
105
134
|
gap: gapProp,
|
|
106
135
|
itemWidth: itemWidthProp,
|
|
136
|
+
itemInset,
|
|
137
|
+
bleedHorizontal,
|
|
107
138
|
paddingHorizontal: paddingHorizontalProp,
|
|
108
139
|
paddingVertical: paddingVerticalProp,
|
|
140
|
+
equalHeight = true,
|
|
141
|
+
maxHeight: maxHeightProp,
|
|
109
142
|
type = 'Default',
|
|
110
143
|
|
|
111
144
|
onIndexChange,
|
|
@@ -127,7 +160,7 @@ export function Carousel({
|
|
|
127
160
|
getVariableByName('carousel/padding/vertical', modes),
|
|
128
161
|
)
|
|
129
162
|
// Outer container max height per Figma (`carousel/maxHeight`).
|
|
130
|
-
const
|
|
163
|
+
const tokenMaxHeight = parseFloat(
|
|
131
164
|
getVariableByName('carousel/maxHeight', modes),
|
|
132
165
|
)
|
|
133
166
|
// Gap between AutoplayControl and NumberPagination in the Numbered overlay.
|
|
@@ -165,15 +198,56 @@ export function Carousel({
|
|
|
165
198
|
|
|
166
199
|
const totalItems = items.length
|
|
167
200
|
|
|
168
|
-
// Effective item width:
|
|
201
|
+
// Effective item width: explicit prop, itemInset derivation, or token peek.
|
|
169
202
|
// For the Numbered variant the items are full-bleed (no peek padding).
|
|
170
203
|
const effectiveItemWidth = useMemo(() => {
|
|
171
204
|
if (itemWidthProp != null) return itemWidthProp
|
|
172
205
|
if (containerWidth === 0) return 0
|
|
173
206
|
if (isNumbered) return containerWidth
|
|
207
|
+
if (itemInset != null) {
|
|
208
|
+
return Math.max(0, containerWidth - itemInset * 2)
|
|
209
|
+
}
|
|
174
210
|
// Full-width minus peekOffset on each side
|
|
175
211
|
return containerWidth - containerPaddingH * 4
|
|
176
|
-
}, [itemWidthProp, containerWidth, containerPaddingH, isNumbered])
|
|
212
|
+
}, [itemWidthProp, itemInset, containerWidth, containerPaddingH, isNumbered])
|
|
213
|
+
|
|
214
|
+
// Equal-height: measure each slide's intrinsic height, then lock every slot
|
|
215
|
+
// to the tallest. Do NOT use contentContainerStyle.alignItems:'stretch' or
|
|
216
|
+
// height:'100%' without a bounded cross-axis — in a horizontal ScrollView
|
|
217
|
+
// that resolves to an unbounded/infinite height.
|
|
218
|
+
const slideHeightsRef = useRef<Record<number, number>>({})
|
|
219
|
+
const [equalizedHeight, setEqualizedHeight] = useState<number | undefined>()
|
|
220
|
+
const equalizedHeightRef = useRef<number | undefined>(undefined)
|
|
221
|
+
equalizedHeightRef.current = equalizedHeight
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
slideHeightsRef.current = {}
|
|
225
|
+
equalizedHeightRef.current = undefined
|
|
226
|
+
setEqualizedHeight(undefined)
|
|
227
|
+
}, [totalItems, effectiveItemWidth, equalHeight])
|
|
228
|
+
|
|
229
|
+
const handleSlideLayout = useCallback(
|
|
230
|
+
(index: number) => (event: LayoutChangeEvent) => {
|
|
231
|
+
if (!equalHeight) return
|
|
232
|
+
// Only measure while unconstrained. After lock, the slot height is forced
|
|
233
|
+
// and onLayout would just echo equalizedHeight.
|
|
234
|
+
if (equalizedHeightRef.current != null) return
|
|
235
|
+
|
|
236
|
+
const height = Math.ceil(event.nativeEvent.layout.height)
|
|
237
|
+
if (height <= 0) return
|
|
238
|
+
|
|
239
|
+
const previous = slideHeightsRef.current[index] ?? 0
|
|
240
|
+
if (height <= previous) return
|
|
241
|
+
|
|
242
|
+
slideHeightsRef.current[index] = height
|
|
243
|
+
const measured = items.map((_, i) => slideHeightsRef.current[i])
|
|
244
|
+
if (measured.some(value => value == null)) return
|
|
245
|
+
|
|
246
|
+
const next = Math.max(...(measured as number[]))
|
|
247
|
+
setEqualizedHeight(next)
|
|
248
|
+
},
|
|
249
|
+
[equalHeight, items],
|
|
250
|
+
)
|
|
177
251
|
|
|
178
252
|
// Snap interval = item width + gap
|
|
179
253
|
const snapInterval = effectiveItemWidth + gap
|
|
@@ -345,15 +419,31 @@ export function Carousel({
|
|
|
345
419
|
}, [isPlaying, startAutoPlay, clearAutoPlay])
|
|
346
420
|
|
|
347
421
|
// ---- Render ----
|
|
348
|
-
//
|
|
349
|
-
// token
|
|
350
|
-
|
|
351
|
-
|
|
422
|
+
// Padding priority: Numbered → 0; itemInset → inset; explicit padding →
|
|
423
|
+
// verbatim; else token (legacy ×2 peek is applied via item width, not here).
|
|
424
|
+
const effectivePaddingH = isNumbered
|
|
425
|
+
? 0
|
|
426
|
+
: (itemInset ?? paddingHorizontalProp ?? containerPaddingH)
|
|
352
427
|
const effectivePaddingV = isNumbered ? 0 : (paddingVerticalProp ?? containerPaddingV)
|
|
353
428
|
|
|
429
|
+
// Equal-height rows hug content — don't clip with the token maxHeight unless
|
|
430
|
+
// the caller opts into a numeric max (or disables equalHeight for legacy strips).
|
|
431
|
+
const resolvedMaxHeight = isNumbered
|
|
432
|
+
? undefined
|
|
433
|
+
: maxHeightProp === false
|
|
434
|
+
? undefined
|
|
435
|
+
: typeof maxHeightProp === 'number'
|
|
436
|
+
? maxHeightProp
|
|
437
|
+
: equalHeight
|
|
438
|
+
? undefined
|
|
439
|
+
: tokenMaxHeight
|
|
440
|
+
|
|
354
441
|
const outerStyle: ViewStyle = {
|
|
355
442
|
paddingVertical: effectivePaddingV,
|
|
356
|
-
maxHeight:
|
|
443
|
+
maxHeight: resolvedMaxHeight,
|
|
444
|
+
...(bleedHorizontal != null
|
|
445
|
+
? { marginHorizontal: -Math.abs(bleedHorizontal) }
|
|
446
|
+
: {}),
|
|
357
447
|
// Numbered needs relative positioning for the floating controls overlay.
|
|
358
448
|
...(isNumbered ? { overflow: 'hidden' as const } : {}),
|
|
359
449
|
}
|
|
@@ -361,7 +451,8 @@ export function Carousel({
|
|
|
361
451
|
const contentContainerStyle: ViewStyle = {
|
|
362
452
|
paddingHorizontal: effectivePaddingH,
|
|
363
453
|
gap,
|
|
364
|
-
//
|
|
454
|
+
// Always flex-start on the cross axis. Stretch + unbounded ScrollView
|
|
455
|
+
// height is what caused infinite-tall slides.
|
|
365
456
|
alignItems: 'flex-start',
|
|
366
457
|
}
|
|
367
458
|
|
|
@@ -384,6 +475,9 @@ export function Carousel({
|
|
|
384
475
|
onMomentumScrollEnd={finishProgrammaticScroll}
|
|
385
476
|
>
|
|
386
477
|
{items.map((child, index) => {
|
|
478
|
+
const hasEqualizedHeight =
|
|
479
|
+
equalHeight && equalizedHeight != null && equalizedHeight > 0
|
|
480
|
+
|
|
387
481
|
// Strict slot box: width must be honored; never grow or shrink with
|
|
388
482
|
// content, and clip anything that misbehaves (e.g. a child whose
|
|
389
483
|
// inner flex layout would otherwise leak into the next slot on
|
|
@@ -394,6 +488,7 @@ export function Carousel({
|
|
|
394
488
|
flexShrink: 0,
|
|
395
489
|
flexBasis: effectiveItemWidth > 0 ? effectiveItemWidth : 'auto',
|
|
396
490
|
overflow: 'hidden',
|
|
491
|
+
...(hasEqualizedHeight ? { height: equalizedHeight } : {}),
|
|
397
492
|
}
|
|
398
493
|
|
|
399
494
|
// The cloned style forces the child's outer node to also honor the
|
|
@@ -404,6 +499,9 @@ export function Carousel({
|
|
|
404
499
|
maxWidth: effectiveItemWidth > 0 ? effectiveItemWidth : undefined,
|
|
405
500
|
flexGrow: 0,
|
|
406
501
|
flexShrink: 0,
|
|
502
|
+
...(hasEqualizedHeight
|
|
503
|
+
? { height: '100%' as const, alignSelf: 'stretch' }
|
|
504
|
+
: {}),
|
|
407
505
|
}
|
|
408
506
|
|
|
409
507
|
// Pass modes down to children
|
|
@@ -419,7 +517,17 @@ export function Carousel({
|
|
|
419
517
|
|
|
420
518
|
return (
|
|
421
519
|
<View key={index} style={slotStyle}>
|
|
422
|
-
{
|
|
520
|
+
{/*
|
|
521
|
+
Measure intrinsic height on an unconstrained inner wrapper.
|
|
522
|
+
After equalization the outer slot is height-locked and this
|
|
523
|
+
inner fills it — further onLayouts are ignored.
|
|
524
|
+
*/}
|
|
525
|
+
<View
|
|
526
|
+
style={hasEqualizedHeight ? styles.equalizedFill : undefined}
|
|
527
|
+
onLayout={handleSlideLayout(index)}
|
|
528
|
+
>
|
|
529
|
+
{childWithModes}
|
|
530
|
+
</View>
|
|
423
531
|
</View>
|
|
424
532
|
)
|
|
425
533
|
})}
|
|
@@ -612,6 +720,12 @@ function AnimatedDot({
|
|
|
612
720
|
)
|
|
613
721
|
}
|
|
614
722
|
|
|
723
|
+
const styles = StyleSheet.create({
|
|
724
|
+
equalizedFill: {
|
|
725
|
+
flex: 1,
|
|
726
|
+
},
|
|
727
|
+
})
|
|
728
|
+
|
|
615
729
|
// ---------------------------------------------------------------------------
|
|
616
730
|
// Attach sub-components
|
|
617
731
|
// ---------------------------------------------------------------------------
|