jfs-components 0.0.70 → 0.0.72
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 +49 -0
- package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
- package/lib/commonjs/components/CardFinancialCondition/CardFinancialCondition.js +213 -0
- package/lib/commonjs/components/Carousel/Carousel.js +9 -7
- package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +147 -0
- package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +258 -0
- package/lib/commonjs/components/CircularRating/CircularRating.js +161 -0
- package/lib/commonjs/components/Gauge/Gauge.js +223 -0
- package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -2
- package/lib/commonjs/components/InstitutionBadge/InstitutionBadge.js +132 -0
- package/lib/commonjs/components/ListGroup/ListGroup.js +3 -1
- package/lib/commonjs/components/Nudge/Nudge.js +179 -87
- package/lib/commonjs/components/Radio/Radio.js +194 -0
- package/lib/commonjs/components/RadioButton/RadioButton.js +21 -188
- package/lib/commonjs/components/index.js +56 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CardAdvisory/CardAdvisory.js +197 -0
- package/lib/module/components/CardCTA/CardCTA.js +199 -17
- package/lib/module/components/CardFinancialCondition/CardFinancialCondition.js +207 -0
- package/lib/module/components/Carousel/Carousel.js +9 -7
- package/lib/module/components/CircularProgressBar/CircularProgressBar.js +141 -0
- package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +253 -0
- package/lib/module/components/CircularRating/CircularRating.js +155 -0
- package/lib/module/components/Gauge/Gauge.js +217 -0
- package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -2
- package/lib/module/components/InstitutionBadge/InstitutionBadge.js +127 -0
- package/lib/module/components/ListGroup/ListGroup.js +3 -1
- package/lib/module/components/Nudge/Nudge.js +178 -87
- package/lib/module/components/Radio/Radio.js +188 -0
- package/lib/module/components/RadioButton/RadioButton.js +20 -185
- package/lib/module/components/index.js +12 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CardAdvisory/CardAdvisory.d.ts +49 -0
- package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +16 -1
- package/lib/typescript/src/components/CardFinancialCondition/CardFinancialCondition.d.ts +50 -0
- package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +27 -0
- package/lib/typescript/src/components/CircularProgressBarDoted/CircularProgressBarDoted.d.ts +48 -0
- package/lib/typescript/src/components/CircularRating/CircularRating.d.ts +49 -0
- package/lib/typescript/src/components/Gauge/Gauge.d.ts +53 -0
- package/lib/typescript/src/components/InstitutionBadge/InstitutionBadge.d.ts +30 -0
- package/lib/typescript/src/components/Nudge/Nudge.d.ts +14 -11
- package/lib/typescript/src/components/Radio/Radio.d.ts +30 -0
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +20 -28
- package/lib/typescript/src/components/index.d.ts +13 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CardAdvisory/CardAdvisory.tsx +283 -0
- package/src/components/CardCTA/CardCTA.tsx +236 -13
- package/src/components/CardFinancialCondition/CardFinancialCondition.tsx +366 -0
- package/src/components/Carousel/Carousel.tsx +14 -6
- package/src/components/CircularProgressBar/CircularProgressBar.tsx +190 -0
- package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +357 -0
- package/src/components/CircularRating/CircularRating.tsx +241 -0
- package/src/components/Gauge/Gauge.tsx +303 -0
- package/src/components/HoldingsCard/HoldingsCard.tsx +2 -2
- package/src/components/InstitutionBadge/InstitutionBadge.tsx +216 -0
- package/src/components/ListGroup/ListGroup.tsx +3 -1
- package/src/components/Nudge/Nudge.tsx +222 -82
- package/src/components/Radio/Radio.tsx +227 -0
- package/src/components/RadioButton/RadioButton.tsx +23 -225
- package/src/components/index.ts +13 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Text,
|
|
4
|
+
View,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type TextStyle,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
11
|
+
import Icon from '../../icons/Icon'
|
|
12
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
13
|
+
import CircularProgressBar from '../CircularProgressBar/CircularProgressBar'
|
|
14
|
+
import Nudge from '../Nudge/Nudge'
|
|
15
|
+
|
|
16
|
+
type CardAdvisoryBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
|
|
17
|
+
|
|
18
|
+
export type CardAdvisoryProps = CardAdvisoryBaseProps & {
|
|
19
|
+
/** Main heading text. */
|
|
20
|
+
title?: string
|
|
21
|
+
/** Description shown below the heading. */
|
|
22
|
+
description?: string
|
|
23
|
+
/** Progress score displayed in the circular progress bar. */
|
|
24
|
+
value?: number
|
|
25
|
+
/** Optional formatted score label. */
|
|
26
|
+
valueLabel?: string
|
|
27
|
+
/** Show the info icon beside the title. */
|
|
28
|
+
showInfoIcon?: boolean
|
|
29
|
+
/** Show the bottom advisory nudge. */
|
|
30
|
+
showNudge?: boolean
|
|
31
|
+
/** Body text for the default nudge. */
|
|
32
|
+
nudgeBody?: string
|
|
33
|
+
/** Button label for the default nudge. */
|
|
34
|
+
nudgeButtonLabel?: string
|
|
35
|
+
/** Callback for the default nudge button. */
|
|
36
|
+
onPressNudgeButton?: () => void
|
|
37
|
+
/** Optional slot replacing the title info icon. Receives `modes` recursively. */
|
|
38
|
+
titleEndSlot?: React.ReactNode
|
|
39
|
+
/** Optional slot replacing the circular progress bar. Receives `modes` recursively. */
|
|
40
|
+
progressSlot?: React.ReactNode
|
|
41
|
+
/** Optional slot replacing the bottom nudge. Receives `modes` recursively. */
|
|
42
|
+
nudgeSlot?: React.ReactNode
|
|
43
|
+
/** Design token modes forwarded to token lookups and child components. */
|
|
44
|
+
modes?: Record<string, any>
|
|
45
|
+
/** Optional container style override. */
|
|
46
|
+
style?: StyleProp<ViewStyle>
|
|
47
|
+
/** Optional main content row style override. */
|
|
48
|
+
mainContentStyle?: StyleProp<ViewStyle>
|
|
49
|
+
/** Optional title text style override. */
|
|
50
|
+
titleStyle?: StyleProp<TextStyle>
|
|
51
|
+
/** Optional description text style override. */
|
|
52
|
+
descriptionStyle?: StyleProp<TextStyle>
|
|
53
|
+
/** Optional progress wrapper style override. */
|
|
54
|
+
progressStyle?: StyleProp<ViewStyle>
|
|
55
|
+
/** Optional nudge style override. */
|
|
56
|
+
nudgeStyle?: StyleProp<ViewStyle>
|
|
57
|
+
/** Accessibility label for the full card. */
|
|
58
|
+
accessibilityLabel?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CardAdvisoryTokens {
|
|
62
|
+
containerStyle: ViewStyle
|
|
63
|
+
mainContentStyle: ViewStyle
|
|
64
|
+
contentStyle: ViewStyle
|
|
65
|
+
headerStyle: ViewStyle
|
|
66
|
+
titleStyle: TextStyle
|
|
67
|
+
descriptionStyle: TextStyle
|
|
68
|
+
iconColor: string
|
|
69
|
+
iconSize: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
73
|
+
if (typeof value === 'number') {
|
|
74
|
+
return Number.isFinite(value) ? value : fallback
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
const parsed = Number(value)
|
|
79
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return fallback
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
|
|
86
|
+
if (typeof value === 'number') {
|
|
87
|
+
return String(value) as TextStyle['fontWeight']
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
return value as TextStyle['fontWeight']
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return fallback
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveCardAdvisoryTokens(modes: Record<string, any>): CardAdvisoryTokens {
|
|
98
|
+
const width = toNumber(getVariableByName('cardAdvisory/width', modes), 360)
|
|
99
|
+
const gap = toNumber(getVariableByName('cardAdvisory/gap', modes), 16)
|
|
100
|
+
const paddingHorizontal = toNumber(getVariableByName('cardAdvisory/padding/horizontal', modes), 0)
|
|
101
|
+
const paddingVertical = toNumber(getVariableByName('cardAdvisory/padding/vertical', modes), 0)
|
|
102
|
+
const radius = toNumber(getVariableByName('cardAdvisory/radius', modes), 0)
|
|
103
|
+
const background = getVariableByName('cardAdvisory/background', modes) || '#ffffff'
|
|
104
|
+
|
|
105
|
+
const mainContentGap = toNumber(getVariableByName('cardAdvisory/mainContent/gap', modes), 16)
|
|
106
|
+
const contentGap = toNumber(getVariableByName('cardAdvisory/content/gap', modes), 6)
|
|
107
|
+
const headerGap = toNumber(getVariableByName('cardAdvisory/header/gap', modes), 8)
|
|
108
|
+
|
|
109
|
+
const titleColor = getVariableByName('cardAdvisory/title/foreground', modes) || '#0d0d0f'
|
|
110
|
+
const titleFontSize = toNumber(getVariableByName('cardAdvisory/title/fontSize', modes), 26)
|
|
111
|
+
const titleFontFamily = getVariableByName('cardAdvisory/title/fontFamily', modes) || 'JioType Var'
|
|
112
|
+
const titleLineHeight = toNumber(getVariableByName('cardAdvisory/title/lineHeight', modes), 26)
|
|
113
|
+
const titleFontWeight = toFontWeight(getVariableByName('cardAdvisory/title/fontWeight', modes), '900')
|
|
114
|
+
const titleDescenderAllowance = Math.ceil(titleFontSize * 0.16)
|
|
115
|
+
|
|
116
|
+
const descriptionColor = getVariableByName('cardAdvisory/description/foreground', modes) || '#24262b'
|
|
117
|
+
const descriptionFontSize = toNumber(getVariableByName('cardAdvisory/description/fontSize', modes), 12)
|
|
118
|
+
const descriptionFontFamily = getVariableByName('cardAdvisory/description/fontFamily', modes) || 'JioType Var'
|
|
119
|
+
const descriptionLineHeight = toNumber(getVariableByName('cardAdvisory/description/lineHeight', modes), 16)
|
|
120
|
+
const descriptionFontWeight = toFontWeight(
|
|
121
|
+
getVariableByName('cardAdvisory/description/fontWeight', modes),
|
|
122
|
+
'500'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
containerStyle: {
|
|
127
|
+
alignItems: 'flex-start',
|
|
128
|
+
backgroundColor: background as string,
|
|
129
|
+
borderRadius: radius,
|
|
130
|
+
gap,
|
|
131
|
+
overflow: 'hidden',
|
|
132
|
+
paddingHorizontal,
|
|
133
|
+
paddingVertical,
|
|
134
|
+
width,
|
|
135
|
+
},
|
|
136
|
+
mainContentStyle: {
|
|
137
|
+
alignItems: 'flex-start',
|
|
138
|
+
flexDirection: 'row',
|
|
139
|
+
gap: mainContentGap,
|
|
140
|
+
width: '100%',
|
|
141
|
+
},
|
|
142
|
+
contentStyle: {
|
|
143
|
+
alignItems: 'flex-start',
|
|
144
|
+
flex: 1,
|
|
145
|
+
gap: contentGap,
|
|
146
|
+
minWidth: 1,
|
|
147
|
+
},
|
|
148
|
+
headerStyle: {
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
flexDirection: 'row',
|
|
151
|
+
gap: headerGap,
|
|
152
|
+
width: '100%',
|
|
153
|
+
},
|
|
154
|
+
titleStyle: {
|
|
155
|
+
color: titleColor as string,
|
|
156
|
+
fontFamily: titleFontFamily as string,
|
|
157
|
+
fontSize: titleFontSize,
|
|
158
|
+
fontWeight: titleFontWeight,
|
|
159
|
+
lineHeight: titleLineHeight,
|
|
160
|
+
marginBottom: -titleDescenderAllowance,
|
|
161
|
+
paddingBottom: titleDescenderAllowance,
|
|
162
|
+
},
|
|
163
|
+
descriptionStyle: {
|
|
164
|
+
color: descriptionColor as string,
|
|
165
|
+
fontFamily: descriptionFontFamily as string,
|
|
166
|
+
fontSize: descriptionFontSize,
|
|
167
|
+
fontWeight: descriptionFontWeight,
|
|
168
|
+
lineHeight: descriptionLineHeight,
|
|
169
|
+
width: '100%',
|
|
170
|
+
},
|
|
171
|
+
iconColor: (getVariableByName('cardAdvisory/icon/color', modes) || '#1a1c1f') as string,
|
|
172
|
+
iconSize: toNumber(getVariableByName('cardAdvisory/icon/size', modes), 18),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function CardAdvisory({
|
|
177
|
+
title = 'Spending',
|
|
178
|
+
description = 'Track your spending habits and stay within your budget.',
|
|
179
|
+
value = 70,
|
|
180
|
+
valueLabel,
|
|
181
|
+
showInfoIcon = true,
|
|
182
|
+
showNudge = true,
|
|
183
|
+
nudgeBody = 'Data confidence is low, add more accounts for better insights.',
|
|
184
|
+
nudgeButtonLabel = 'Button',
|
|
185
|
+
onPressNudgeButton,
|
|
186
|
+
titleEndSlot,
|
|
187
|
+
progressSlot,
|
|
188
|
+
nudgeSlot,
|
|
189
|
+
modes: propModes = EMPTY_MODES,
|
|
190
|
+
style,
|
|
191
|
+
mainContentStyle,
|
|
192
|
+
titleStyle,
|
|
193
|
+
descriptionStyle,
|
|
194
|
+
progressStyle,
|
|
195
|
+
nudgeStyle,
|
|
196
|
+
accessibilityLabel,
|
|
197
|
+
...rest
|
|
198
|
+
}: CardAdvisoryProps) {
|
|
199
|
+
const { modes: globalModes } = useTokens()
|
|
200
|
+
const modes = useMemo(
|
|
201
|
+
() => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
202
|
+
? EMPTY_MODES
|
|
203
|
+
: { ...globalModes, ...propModes }),
|
|
204
|
+
[globalModes, propModes]
|
|
205
|
+
)
|
|
206
|
+
const tokens = useMemo(() => resolveCardAdvisoryTokens(modes), [modes])
|
|
207
|
+
|
|
208
|
+
const processedTitleEndSlot = useMemo(() => {
|
|
209
|
+
if (!titleEndSlot) return null
|
|
210
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(titleEndSlot), modes)
|
|
211
|
+
return processed.length === 1 ? processed[0] : processed
|
|
212
|
+
}, [titleEndSlot, modes])
|
|
213
|
+
|
|
214
|
+
const processedProgressSlot = useMemo(() => {
|
|
215
|
+
if (!progressSlot) return null
|
|
216
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(progressSlot), modes)
|
|
217
|
+
return processed.length === 1 ? processed[0] : processed
|
|
218
|
+
}, [progressSlot, modes])
|
|
219
|
+
|
|
220
|
+
const processedNudgeSlot = useMemo(() => {
|
|
221
|
+
if (!nudgeSlot) return null
|
|
222
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(nudgeSlot), modes)
|
|
223
|
+
return processed.length === 1 ? processed[0] : processed
|
|
224
|
+
}, [nudgeSlot, modes])
|
|
225
|
+
|
|
226
|
+
const defaultAccessibilityLabel =
|
|
227
|
+
accessibilityLabel ?? `${title}. ${description}. ${Math.round(value)} out of 100. ${nudgeBody}`
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<View
|
|
231
|
+
accessibilityLabel={defaultAccessibilityLabel}
|
|
232
|
+
style={[tokens.containerStyle, style]}
|
|
233
|
+
{...rest}
|
|
234
|
+
>
|
|
235
|
+
<View style={[tokens.mainContentStyle, mainContentStyle]}>
|
|
236
|
+
<View style={tokens.contentStyle}>
|
|
237
|
+
<View style={tokens.headerStyle}>
|
|
238
|
+
<Text numberOfLines={1} style={[tokens.titleStyle, titleStyle]}>
|
|
239
|
+
{title}
|
|
240
|
+
</Text>
|
|
241
|
+
{processedTitleEndSlot || (showInfoIcon ? (
|
|
242
|
+
<Icon
|
|
243
|
+
name="ic_info"
|
|
244
|
+
size={tokens.iconSize}
|
|
245
|
+
color={tokens.iconColor}
|
|
246
|
+
accessibilityElementsHidden={true}
|
|
247
|
+
importantForAccessibility="no"
|
|
248
|
+
/>
|
|
249
|
+
) : null)}
|
|
250
|
+
</View>
|
|
251
|
+
<Text style={[tokens.descriptionStyle, descriptionStyle]}>
|
|
252
|
+
{description}
|
|
253
|
+
</Text>
|
|
254
|
+
</View>
|
|
255
|
+
|
|
256
|
+
{processedProgressSlot || (
|
|
257
|
+
<CircularProgressBar
|
|
258
|
+
state="Active"
|
|
259
|
+
value={value}
|
|
260
|
+
modes={modes}
|
|
261
|
+
style={progressStyle}
|
|
262
|
+
{...(valueLabel ? { valueLabel } : {})}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</View>
|
|
266
|
+
|
|
267
|
+
{showNudge ? (
|
|
268
|
+
processedNudgeSlot || (
|
|
269
|
+
<Nudge
|
|
270
|
+
type="inline-compact"
|
|
271
|
+
body={nudgeBody}
|
|
272
|
+
buttonLabel={nudgeButtonLabel}
|
|
273
|
+
modes={modes}
|
|
274
|
+
style={[{ width: '100%' }, nudgeStyle]}
|
|
275
|
+
{...(onPressNudgeButton ? { onPressButton: onPressNudgeButton } : {})}
|
|
276
|
+
/>
|
|
277
|
+
)
|
|
278
|
+
) : null}
|
|
279
|
+
</View>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default React.memo(CardAdvisory)
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, type ViewStyle, type TextStyle, type StyleProp } from 'react-native'
|
|
3
|
-
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
3
|
+
import { findVariablesByPattern, getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
4
|
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
5
|
import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils'
|
|
6
6
|
import IconCapsule from '../IconCapsule/IconCapsule'
|
|
7
7
|
import Button from '../Button/Button'
|
|
8
|
+
import Badge from '../Badge/Badge'
|
|
9
|
+
import ButtonGroup from '../ButtonGroup/ButtonGroup'
|
|
10
|
+
import IconButton from '../IconButton/IconButton'
|
|
11
|
+
|
|
12
|
+
export type CardCTAType = 'cta' | 'rating'
|
|
13
|
+
|
|
14
|
+
const optionalTokenAvailability = new Map<string, boolean>()
|
|
15
|
+
|
|
16
|
+
function getOptionalVariableByName<T>(name: string, modes: Record<string, any>, fallback: T): T {
|
|
17
|
+
let isAvailable = optionalTokenAvailability.get(name)
|
|
18
|
+
|
|
19
|
+
if (isAvailable === undefined) {
|
|
20
|
+
isAvailable = findVariablesByPattern(name).some((variable) => variable.name === name)
|
|
21
|
+
optionalTokenAvailability.set(name, isAvailable)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!isAvailable) {
|
|
25
|
+
return fallback
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (getVariableByName(name, modes) ?? fallback) as T
|
|
29
|
+
}
|
|
8
30
|
|
|
9
31
|
export type CardCTAProps = {
|
|
32
|
+
/** Visual layout variant */
|
|
33
|
+
type?: CardCTAType;
|
|
10
34
|
/** Title text */
|
|
11
35
|
title?: string;
|
|
12
36
|
/** Body / subtitle text */
|
|
@@ -17,29 +41,49 @@ export type CardCTAProps = {
|
|
|
17
41
|
buttonLabel?: string;
|
|
18
42
|
/** Callback for the default Button press */
|
|
19
43
|
onPressButton?: () => void;
|
|
44
|
+
/** Label shown in the rating badge */
|
|
45
|
+
ratingLabel?: string;
|
|
46
|
+
/** Show like/dislike actions in the rating footer */
|
|
47
|
+
showRatingActions?: boolean;
|
|
48
|
+
/** Callback for the default like action */
|
|
49
|
+
onPressLike?: () => void;
|
|
50
|
+
/** Callback for the default dislike action */
|
|
51
|
+
onPressDislike?: () => void;
|
|
20
52
|
/** Mode configuration for design token resolution */
|
|
21
53
|
modes?: Record<string, any>;
|
|
22
54
|
/** Slot: replaces the default icon area (right side) */
|
|
23
55
|
iconSlot?: React.ReactNode;
|
|
24
56
|
/** Slot: replaces the default Button */
|
|
25
57
|
buttonSlot?: React.ReactNode;
|
|
58
|
+
/** Slot: replaces the default rating badge */
|
|
59
|
+
ratingBadgeSlot?: React.ReactNode;
|
|
60
|
+
/** Slot: replaces the default like/dislike action group */
|
|
61
|
+
ratingActionsSlot?: React.ReactNode;
|
|
26
62
|
/** Container style overrides */
|
|
27
63
|
style?: StyleProp<ViewStyle>;
|
|
28
64
|
};
|
|
29
65
|
|
|
30
66
|
function CardCTA({
|
|
67
|
+
type = 'cta',
|
|
31
68
|
title = 'If you have 1 line',
|
|
32
69
|
body = 'Then you can have up to 3 lines in the subtext as well. This is for demonstration.',
|
|
33
70
|
iconName = 'ic_upi_number',
|
|
34
|
-
buttonLabel
|
|
71
|
+
buttonLabel,
|
|
35
72
|
onPressButton,
|
|
73
|
+
ratingLabel = '+28 Rating',
|
|
74
|
+
showRatingActions = true,
|
|
75
|
+
onPressLike,
|
|
76
|
+
onPressDislike,
|
|
36
77
|
modes: propModes = EMPTY_MODES,
|
|
37
78
|
iconSlot,
|
|
38
79
|
buttonSlot,
|
|
80
|
+
ratingBadgeSlot,
|
|
81
|
+
ratingActionsSlot,
|
|
39
82
|
style,
|
|
40
83
|
}: CardCTAProps) {
|
|
41
84
|
const { modes: globalModes } = useTokens()
|
|
42
85
|
const modes = { ...globalModes, ...propModes }
|
|
86
|
+
const isRating = type === 'rating'
|
|
43
87
|
|
|
44
88
|
const background = getVariableByName('cardCTA/background', modes) || '#ffffff'
|
|
45
89
|
const radius = getVariableByName('cardCTA/radius', modes) || 12
|
|
@@ -69,13 +113,50 @@ function CardCTA({
|
|
|
69
113
|
const bodyFontWeightRaw = getVariableByName('cardCTA/body/fontWeight', modes) || 400
|
|
70
114
|
const bodyFontWeight = typeof bodyFontWeightRaw === 'number' ? bodyFontWeightRaw.toString() : bodyFontWeightRaw
|
|
71
115
|
|
|
116
|
+
const ratingContentGap = getOptionalVariableByName('cardCTA/rating/content/gap', modes, 12)
|
|
117
|
+
const ratingContentPaddingH = getOptionalVariableByName('cardCTA/rating/content/padding/horizontal', modes, 16)
|
|
118
|
+
const ratingContentPaddingV = getOptionalVariableByName('cardCTA/rating/content/padding/vertical', modes, 12)
|
|
119
|
+
|
|
120
|
+
const ratingFooterPaddingH = getOptionalVariableByName('cardCTA/rating/footer/horizontal', modes, 16)
|
|
121
|
+
const ratingFooterPaddingTop = getOptionalVariableByName('cardCTA/rating/footer/top', modes, 0)
|
|
122
|
+
const ratingFooterPaddingBottom = getOptionalVariableByName('cardCTA/rating/footer/bottom', modes, 12)
|
|
123
|
+
|
|
124
|
+
const buttonModes = {...modes, AppearanceBrand: 'Secondary', 'Button / Size': 'S'}
|
|
125
|
+
const iconButtonModes = {'Button / Size': 'S', 'Emphasis': 'Low', 'AppearanceBrand':'Neutral',...modes }
|
|
126
|
+
const effectiveButtonLabel = buttonLabel ?? (isRating ? 'Save' : 'Button')
|
|
127
|
+
const nonWrappingButtonLabel = effectiveButtonLabel.replace(/\s/g, '\u00A0')
|
|
128
|
+
const [measuredButtonLabelWidth, setMeasuredButtonLabelWidth] = React.useState<number | null>(null)
|
|
129
|
+
|
|
130
|
+
const buttonPaddingH = getVariableByName('button/padding/horizontal', buttonModes) || 20
|
|
131
|
+
const buttonBorderSize = getVariableByName('button/border/size', buttonModes) ?? 1
|
|
132
|
+
const measuredButtonWidth = measuredButtonLabelWidth === null
|
|
133
|
+
? undefined
|
|
134
|
+
: Math.ceil(measuredButtonLabelWidth + (buttonPaddingH as number) * 2 + (buttonBorderSize as number) * 2)
|
|
135
|
+
|
|
136
|
+
const handleButtonLabelTextLayout = React.useCallback((event: any) => {
|
|
137
|
+
const lines = event?.nativeEvent?.lines
|
|
138
|
+
if (!Array.isArray(lines) || lines.length === 0) return
|
|
139
|
+
|
|
140
|
+
const nextWidth = Math.ceil(
|
|
141
|
+
lines.reduce((sum, line) => sum + (typeof line?.width === 'number' ? line.width : 0), 0)
|
|
142
|
+
)
|
|
143
|
+
if (nextWidth <= 0) return
|
|
144
|
+
|
|
145
|
+
setMeasuredButtonLabelWidth((currentWidth) => {
|
|
146
|
+
if (currentWidth !== null && Math.abs(currentWidth - nextWidth) < 1) {
|
|
147
|
+
return currentWidth
|
|
148
|
+
}
|
|
149
|
+
return nextWidth
|
|
150
|
+
})
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
72
153
|
const containerStyle: ViewStyle = {
|
|
73
154
|
backgroundColor: background,
|
|
74
155
|
borderRadius: radius,
|
|
75
156
|
borderWidth: borderSize,
|
|
76
157
|
borderColor,
|
|
77
|
-
flexDirection: 'row',
|
|
78
|
-
overflow: '
|
|
158
|
+
flexDirection: isRating ? 'column' : 'row',
|
|
159
|
+
overflow: 'visible',
|
|
79
160
|
}
|
|
80
161
|
|
|
81
162
|
// NOTE: `minWidth: 0` + explicit `flexShrink: 1` are required on native.
|
|
@@ -93,6 +174,8 @@ function CardCTA({
|
|
|
93
174
|
gap: leftGap,
|
|
94
175
|
alignItems: 'flex-start',
|
|
95
176
|
justifyContent: 'center',
|
|
177
|
+
overflow: 'visible',
|
|
178
|
+
zIndex: 1,
|
|
96
179
|
}
|
|
97
180
|
|
|
98
181
|
// NOTE: rightWrap must NOT shrink on native. On Android (Yoga), the default
|
|
@@ -123,6 +206,28 @@ function CardCTA({
|
|
|
123
206
|
minWidth: 0,
|
|
124
207
|
}
|
|
125
208
|
|
|
209
|
+
// Keep text shrink/wrap behavior on the left column, but let the CTA keep
|
|
210
|
+
// its own intrinsic width. On native, Yoga otherwise measures the Button
|
|
211
|
+
// with the left column's available width and the single-line label
|
|
212
|
+
// truncates even when the Button itself has no width/maxWidth constraint.
|
|
213
|
+
const buttonWrapStyle: ViewStyle = {
|
|
214
|
+
alignSelf: 'flex-start',
|
|
215
|
+
flexGrow: 0,
|
|
216
|
+
flexShrink: 0,
|
|
217
|
+
flexBasis: 'auto',
|
|
218
|
+
overflow: 'visible',
|
|
219
|
+
zIndex: 1,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const buttonStyle: ViewStyle = {
|
|
223
|
+
alignSelf: 'flex-start',
|
|
224
|
+
flexGrow: 0,
|
|
225
|
+
flexShrink: 0,
|
|
226
|
+
flexBasis: 'auto',
|
|
227
|
+
overflow: 'visible',
|
|
228
|
+
...(measuredButtonWidth !== undefined ? { width: measuredButtonWidth } : {}),
|
|
229
|
+
}
|
|
230
|
+
|
|
126
231
|
const titleStyle: TextStyle = {
|
|
127
232
|
color: titleColor,
|
|
128
233
|
fontFamily: titleFontFamily,
|
|
@@ -139,6 +244,113 @@ function CardCTA({
|
|
|
139
244
|
fontWeight: bodyFontWeight,
|
|
140
245
|
}
|
|
141
246
|
|
|
247
|
+
const ratingContentStyle: ViewStyle = {
|
|
248
|
+
paddingHorizontal: ratingContentPaddingH,
|
|
249
|
+
paddingVertical: ratingContentPaddingV,
|
|
250
|
+
gap: ratingContentGap,
|
|
251
|
+
alignItems: 'flex-start',
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const ratingFooterStyle: ViewStyle = {
|
|
255
|
+
flexDirection: 'row',
|
|
256
|
+
alignItems: 'flex-start',
|
|
257
|
+
justifyContent: 'space-between',
|
|
258
|
+
paddingHorizontal: ratingFooterPaddingH,
|
|
259
|
+
paddingTop: ratingFooterPaddingTop,
|
|
260
|
+
paddingBottom: ratingFooterPaddingBottom,
|
|
261
|
+
overflow: 'visible',
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const buttonLabelStyle: TextStyle = {
|
|
265
|
+
flexGrow: 0,
|
|
266
|
+
flexShrink: 0,
|
|
267
|
+
flexWrap: 'nowrap',
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Keep the rating CTA on an overflow-visible, non-shrinking path. The
|
|
271
|
+
// footer's row width stays fixed by the card, but Yoga must not use that
|
|
272
|
+
// width to shrink or clip the button label.
|
|
273
|
+
const ratingButtonWrapStyle: ViewStyle = {
|
|
274
|
+
flexGrow: 0,
|
|
275
|
+
flexShrink: 0,
|
|
276
|
+
flexBasis: 'auto',
|
|
277
|
+
alignItems: 'flex-start',
|
|
278
|
+
overflow: 'visible',
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ratingButtonStyle: ViewStyle = {
|
|
282
|
+
alignSelf: 'flex-start',
|
|
283
|
+
flexGrow: 0,
|
|
284
|
+
flexShrink: 0,
|
|
285
|
+
flexBasis: 'auto',
|
|
286
|
+
overflow: 'visible',
|
|
287
|
+
...(measuredButtonWidth !== undefined ? { width: measuredButtonWidth } : {}),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const ratingButtonLabelStyle: TextStyle = {
|
|
291
|
+
flexGrow: 0,
|
|
292
|
+
flexShrink: 0,
|
|
293
|
+
flexWrap: 'nowrap',
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isRating) {
|
|
297
|
+
return (
|
|
298
|
+
<View style={[containerStyle, style]}>
|
|
299
|
+
<View style={ratingContentStyle}>
|
|
300
|
+
{ratingBadgeSlot ? (
|
|
301
|
+
cloneChildrenWithModes(ratingBadgeSlot, modes)
|
|
302
|
+
) : (
|
|
303
|
+
<Badge label={ratingLabel} modes={modes} />
|
|
304
|
+
)}
|
|
305
|
+
<View style={textWrapStyle}>
|
|
306
|
+
<Text style={titleStyle}>{title}</Text>
|
|
307
|
+
<Text style={bodyStyle}>{body}</Text>
|
|
308
|
+
</View>
|
|
309
|
+
</View>
|
|
310
|
+
<View style={ratingFooterStyle}>
|
|
311
|
+
<View style={ratingButtonWrapStyle}>
|
|
312
|
+
{buttonSlot ? (
|
|
313
|
+
cloneChildrenWithModes(buttonSlot, buttonModes)
|
|
314
|
+
) : (
|
|
315
|
+
<Button
|
|
316
|
+
label={effectiveButtonLabel}
|
|
317
|
+
onPress={onPressButton || (() => {})}
|
|
318
|
+
modes={buttonModes}
|
|
319
|
+
style={ratingButtonStyle}
|
|
320
|
+
renderContent={(labelStyles) => (
|
|
321
|
+
<Text
|
|
322
|
+
onTextLayout={handleButtonLabelTextLayout}
|
|
323
|
+
style={[labelStyles, ratingButtonLabelStyle]}
|
|
324
|
+
>
|
|
325
|
+
{nonWrappingButtonLabel}
|
|
326
|
+
</Text>
|
|
327
|
+
)}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
</View>
|
|
331
|
+
{showRatingActions ? (
|
|
332
|
+
ratingActionsSlot ? (
|
|
333
|
+
cloneChildrenWithModes(ratingActionsSlot, iconButtonModes)
|
|
334
|
+
) : (
|
|
335
|
+
<ButtonGroup modes={iconButtonModes}>
|
|
336
|
+
<IconButton
|
|
337
|
+
iconName="ic_like"
|
|
338
|
+
accessibilityLabel="Like"
|
|
339
|
+
{...(onPressLike ? { onPress: onPressLike } : {})}
|
|
340
|
+
/>
|
|
341
|
+
<IconButton
|
|
342
|
+
iconName="ic_dislike"
|
|
343
|
+
accessibilityLabel="Dislike"
|
|
344
|
+
{...(onPressDislike ? { onPress: onPressDislike } : {})}
|
|
345
|
+
/>
|
|
346
|
+
</ButtonGroup>
|
|
347
|
+
)
|
|
348
|
+
) : null}
|
|
349
|
+
</View>
|
|
350
|
+
</View>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
142
354
|
return (
|
|
143
355
|
<View style={[containerStyle, style]}>
|
|
144
356
|
<View style={leftWrapStyle}>
|
|
@@ -146,15 +358,26 @@ function CardCTA({
|
|
|
146
358
|
<Text style={titleStyle}>{title}</Text>
|
|
147
359
|
<Text style={bodyStyle}>{body}</Text>
|
|
148
360
|
</View>
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
361
|
+
<View style={buttonWrapStyle}>
|
|
362
|
+
{buttonSlot ? (
|
|
363
|
+
cloneChildrenWithModes(buttonSlot, buttonModes)
|
|
364
|
+
) : (
|
|
365
|
+
<Button
|
|
366
|
+
label={effectiveButtonLabel}
|
|
367
|
+
onPress={onPressButton || (() => {})}
|
|
368
|
+
modes={buttonModes}
|
|
369
|
+
style={buttonStyle}
|
|
370
|
+
renderContent={(labelStyles) => (
|
|
371
|
+
<Text
|
|
372
|
+
onTextLayout={handleButtonLabelTextLayout}
|
|
373
|
+
style={[labelStyles, buttonLabelStyle]}
|
|
374
|
+
>
|
|
375
|
+
{nonWrappingButtonLabel}
|
|
376
|
+
</Text>
|
|
377
|
+
)}
|
|
378
|
+
/>
|
|
379
|
+
)}
|
|
380
|
+
</View>
|
|
158
381
|
</View>
|
|
159
382
|
<View style={rightWrapStyle}>
|
|
160
383
|
{iconSlot ? (
|