jfs-components 0.0.70 → 0.0.71
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/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
- 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/ListGroup/ListGroup.js +3 -1
- package/lib/commonjs/components/Nudge/Nudge.js +179 -87
- package/lib/commonjs/components/index.js +35 -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/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/ListGroup/ListGroup.js +3 -1
- package/lib/module/components/Nudge/Nudge.js +178 -87
- package/lib/module/components/index.js +5 -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/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/Nudge/Nudge.d.ts +14 -11
- package/lib/typescript/src/components/index.d.ts +6 -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/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/ListGroup/ListGroup.tsx +3 -1
- package/src/components/Nudge/Nudge.tsx +222 -82
- package/src/components/index.ts +6 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -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 ? (
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { StyleSheet, Text, View, type StyleProp, type TextStyle, type ViewStyle } from 'react-native'
|
|
3
|
+
import Svg, { Circle } from 'react-native-svg'
|
|
4
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
5
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
6
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
7
|
+
import { IconMinus } from '../../icons/components/IconMinus'
|
|
8
|
+
|
|
9
|
+
type CircularProgressBarBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
|
|
10
|
+
|
|
11
|
+
export type CircularProgressBarState = 'Active' | 'Inactive'
|
|
12
|
+
|
|
13
|
+
export type CircularProgressBarProps = CircularProgressBarBaseProps & {
|
|
14
|
+
/** Current progress value. Clamped between 0 and 100. */
|
|
15
|
+
value?: number
|
|
16
|
+
/** Active shows progress and value; inactive shows the track and minus icon. */
|
|
17
|
+
state?: CircularProgressBarState | boolean
|
|
18
|
+
/** Optional formatted value shown in the active state. */
|
|
19
|
+
valueLabel?: string
|
|
20
|
+
/** Design token modes forwarded to token lookups. */
|
|
21
|
+
modes?: Record<string, any>
|
|
22
|
+
/** Container style override. */
|
|
23
|
+
style?: StyleProp<ViewStyle>
|
|
24
|
+
/** Track stroke style override. */
|
|
25
|
+
trackStyle?: StyleProp<ViewStyle>
|
|
26
|
+
/** Progress stroke style override. */
|
|
27
|
+
progressStyle?: StyleProp<ViewStyle>
|
|
28
|
+
/** Value text style override. */
|
|
29
|
+
valueStyle?: StyleProp<TextStyle>
|
|
30
|
+
/** Accessibility label for the whole progress component. */
|
|
31
|
+
accessibilityLabel?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STROKE_WIDTH_RATIO = 8 / 60
|
|
35
|
+
|
|
36
|
+
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
|
|
37
|
+
|
|
38
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
39
|
+
if (typeof value === 'number') {
|
|
40
|
+
return Number.isFinite(value) ? value : fallback
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
const parsed = Number(value)
|
|
45
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return fallback
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
|
|
52
|
+
if (typeof value === 'number') {
|
|
53
|
+
return String(value) as TextStyle['fontWeight']
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
return value as TextStyle['fontWeight']
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return fallback
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const getStrokeColor = (style: StyleProp<ViewStyle>, fallback: string) => {
|
|
64
|
+
const flattened = StyleSheet.flatten(style)
|
|
65
|
+
return typeof flattened.backgroundColor === 'string'
|
|
66
|
+
? flattened.backgroundColor
|
|
67
|
+
: fallback
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CircularProgressBar({
|
|
71
|
+
value = 70,
|
|
72
|
+
state = 'Inactive',
|
|
73
|
+
valueLabel,
|
|
74
|
+
modes: propModes = EMPTY_MODES,
|
|
75
|
+
style,
|
|
76
|
+
trackStyle,
|
|
77
|
+
progressStyle,
|
|
78
|
+
valueStyle,
|
|
79
|
+
accessibilityLabel,
|
|
80
|
+
...rest
|
|
81
|
+
}: CircularProgressBarProps) {
|
|
82
|
+
const { modes: globalModes } = useTokens()
|
|
83
|
+
const modes = { ...globalModes, ...propModes }
|
|
84
|
+
|
|
85
|
+
const isActive = state === true || state === 'Active'
|
|
86
|
+
const normalizedValue = clamp(value, 0, 100)
|
|
87
|
+
const size = toNumber(getVariableByName('circularProgressBar/size', modes), 60)
|
|
88
|
+
const strokeWidth = Math.max(1, size * STROKE_WIDTH_RATIO)
|
|
89
|
+
const radius = Math.max(0, (size - strokeWidth) / 2)
|
|
90
|
+
const center = size / 2
|
|
91
|
+
const circumference = 2 * Math.PI * radius
|
|
92
|
+
|
|
93
|
+
const trackColor = getStrokeColor(
|
|
94
|
+
trackStyle,
|
|
95
|
+
getVariableByName('circularProgressBar/track/color', modes) as string || '#ebebed'
|
|
96
|
+
)
|
|
97
|
+
const progressColor = getStrokeColor(
|
|
98
|
+
progressStyle,
|
|
99
|
+
getVariableByName('circularProgressBar/progress/color', modes) as string || '#25ab21'
|
|
100
|
+
)
|
|
101
|
+
const iconColor = getVariableByName('circularProgressBar/icon/color', modes) as string || '#666666'
|
|
102
|
+
const iconSize = toNumber(getVariableByName('circularProgressBar/icon/size', modes), 24)
|
|
103
|
+
|
|
104
|
+
const foreground = getVariableByName('circularProgressBar/foreground', modes) as string || '#0d0d0f'
|
|
105
|
+
const fontSize = toNumber(getVariableByName('circularProgressBar/fontSize', modes), 18)
|
|
106
|
+
const fontFamily = getVariableByName('circularProgressBar/fontFamily', modes) as string || 'JioType Var'
|
|
107
|
+
const lineHeight = toNumber(getVariableByName('circularProgressBar/lineHeight', modes), 21)
|
|
108
|
+
const fontWeight = toFontWeight(getVariableByName('circularProgressBar/fontWeight', modes), '700')
|
|
109
|
+
|
|
110
|
+
const computedContainerStyle: ViewStyle = {
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
height: size,
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
position: 'relative',
|
|
115
|
+
width: size,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const computedValueStyle: TextStyle = {
|
|
119
|
+
color: foreground,
|
|
120
|
+
fontFamily,
|
|
121
|
+
fontSize,
|
|
122
|
+
fontWeight,
|
|
123
|
+
lineHeight,
|
|
124
|
+
position: 'absolute',
|
|
125
|
+
textAlign: 'center',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const iconStyle: ViewStyle = {
|
|
129
|
+
left: (size - iconSize) / 2,
|
|
130
|
+
position: 'absolute',
|
|
131
|
+
top: (size - iconSize) / 2,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const displayValue = valueLabel ?? String(Math.round(normalizedValue))
|
|
135
|
+
const defaultAccessibilityLabel =
|
|
136
|
+
accessibilityLabel ?? (isActive ? `${displayValue} out of 100` : 'Inactive progress')
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<View
|
|
140
|
+
accessibilityRole="progressbar"
|
|
141
|
+
accessibilityLabel={defaultAccessibilityLabel}
|
|
142
|
+
accessibilityValue={{ min: 0, max: 100, now: normalizedValue }}
|
|
143
|
+
style={[computedContainerStyle, style]}
|
|
144
|
+
{...rest}
|
|
145
|
+
>
|
|
146
|
+
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
147
|
+
<Circle
|
|
148
|
+
cx={center}
|
|
149
|
+
cy={center}
|
|
150
|
+
r={radius}
|
|
151
|
+
stroke={trackColor}
|
|
152
|
+
strokeWidth={strokeWidth}
|
|
153
|
+
fill="none"
|
|
154
|
+
/>
|
|
155
|
+
{isActive ? (
|
|
156
|
+
<Circle
|
|
157
|
+
cx={center}
|
|
158
|
+
cy={center}
|
|
159
|
+
r={radius}
|
|
160
|
+
stroke={progressColor}
|
|
161
|
+
strokeWidth={strokeWidth}
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
fill="none"
|
|
164
|
+
strokeDasharray={`${circumference} ${circumference}`}
|
|
165
|
+
strokeDashoffset={circumference * (1 - normalizedValue / 100)}
|
|
166
|
+
rotation="-90"
|
|
167
|
+
originX={center}
|
|
168
|
+
originY={center}
|
|
169
|
+
/>
|
|
170
|
+
) : null}
|
|
171
|
+
</Svg>
|
|
172
|
+
|
|
173
|
+
{isActive ? (
|
|
174
|
+
<Text style={[computedValueStyle, valueStyle]}>
|
|
175
|
+
{displayValue}
|
|
176
|
+
</Text>
|
|
177
|
+
) : (
|
|
178
|
+
<IconMinus
|
|
179
|
+
width={iconSize}
|
|
180
|
+
height={iconSize}
|
|
181
|
+
fill={iconColor}
|
|
182
|
+
color={iconColor}
|
|
183
|
+
style={iconStyle}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</View>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default CircularProgressBar
|