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.
- package/CHANGELOG.md +5 -0
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +16 -26
- package/lib/commonjs/components/CardCTA/CardCTA.js +36 -12
- package/lib/commonjs/components/Carousel/Carousel.js +33 -13
- 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 +36 -12
- package/lib/module/components/Carousel/Carousel.js +33 -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 +66 -20
- package/src/components/Carousel/Carousel.tsx +67 -12
- 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:32:46.426Z
|
|
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,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
|
-
{
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
518
|
-
{
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
349
|
-
// token
|
|
350
|
-
|
|
351
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|