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.
Files changed (43) hide show
  1. package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
  2. package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
  3. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +147 -0
  4. package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +258 -0
  5. package/lib/commonjs/components/CircularRating/CircularRating.js +161 -0
  6. package/lib/commonjs/components/Gauge/Gauge.js +223 -0
  7. package/lib/commonjs/components/ListGroup/ListGroup.js +3 -1
  8. package/lib/commonjs/components/Nudge/Nudge.js +179 -87
  9. package/lib/commonjs/components/index.js +35 -0
  10. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/module/components/CardAdvisory/CardAdvisory.js +197 -0
  13. package/lib/module/components/CardCTA/CardCTA.js +199 -17
  14. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +141 -0
  15. package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +253 -0
  16. package/lib/module/components/CircularRating/CircularRating.js +155 -0
  17. package/lib/module/components/Gauge/Gauge.js +217 -0
  18. package/lib/module/components/ListGroup/ListGroup.js +3 -1
  19. package/lib/module/components/Nudge/Nudge.js +178 -87
  20. package/lib/module/components/index.js +5 -0
  21. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  22. package/lib/module/icons/registry.js +1 -1
  23. package/lib/typescript/src/components/CardAdvisory/CardAdvisory.d.ts +49 -0
  24. package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +16 -1
  25. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +27 -0
  26. package/lib/typescript/src/components/CircularProgressBarDoted/CircularProgressBarDoted.d.ts +48 -0
  27. package/lib/typescript/src/components/CircularRating/CircularRating.d.ts +49 -0
  28. package/lib/typescript/src/components/Gauge/Gauge.d.ts +53 -0
  29. package/lib/typescript/src/components/Nudge/Nudge.d.ts +14 -11
  30. package/lib/typescript/src/components/index.d.ts +6 -1
  31. package/lib/typescript/src/icons/registry.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/src/components/CardAdvisory/CardAdvisory.tsx +283 -0
  34. package/src/components/CardCTA/CardCTA.tsx +236 -13
  35. package/src/components/CircularProgressBar/CircularProgressBar.tsx +190 -0
  36. package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +357 -0
  37. package/src/components/CircularRating/CircularRating.tsx +241 -0
  38. package/src/components/Gauge/Gauge.tsx +303 -0
  39. package/src/components/ListGroup/ListGroup.tsx +3 -1
  40. package/src/components/Nudge/Nudge.tsx +222 -82
  41. package/src/components/index.ts +6 -1
  42. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  43. 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 = 'Button',
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: 'hidden',
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
- {buttonSlot ? (
150
- cloneChildrenWithModes(buttonSlot, {...modes, AppearanceBrand: 'Secondary', 'Button / Size': 'S'})
151
- ) : (
152
- <Button
153
- label={buttonLabel}
154
- onPress={onPressButton || (() => {})}
155
- modes={{...modes, AppearanceBrand: 'Secondary', 'Button / Size': 'S'}}
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