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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
type LayoutChangeEvent,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
13
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
14
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
15
|
+
import { IconChevronright } from '../../icons/components/IconChevronright'
|
|
16
|
+
|
|
17
|
+
type CircularProgressBarDotedBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
|
|
18
|
+
|
|
19
|
+
export type CircularProgressBarDotedProps = CircularProgressBarDotedBaseProps & {
|
|
20
|
+
/** Progress value. Clamped between 0 and 100. */
|
|
21
|
+
value?: number
|
|
22
|
+
/** Number of dots in the ring. */
|
|
23
|
+
dotCount?: number
|
|
24
|
+
/** Small label above the score. */
|
|
25
|
+
label?: string
|
|
26
|
+
/** Text below the score. */
|
|
27
|
+
tierLabel?: string
|
|
28
|
+
/** Show or hide the chevron after the tier label. */
|
|
29
|
+
showChevron?: boolean
|
|
30
|
+
/** Called when the component is pressed. */
|
|
31
|
+
onPress?: () => void
|
|
32
|
+
/** Called when the tier row is pressed. */
|
|
33
|
+
onTierPress?: () => void
|
|
34
|
+
/** Design token modes forwarded to token lookups and slot children. */
|
|
35
|
+
modes?: Record<string, any>
|
|
36
|
+
/** Slot rendered in the center of the dotted ring. Receives `modes` recursively. */
|
|
37
|
+
children?: React.ReactNode
|
|
38
|
+
/** Container style override. */
|
|
39
|
+
style?: StyleProp<ViewStyle>
|
|
40
|
+
/** Ring wrapper style override. */
|
|
41
|
+
ringStyle?: StyleProp<ViewStyle>
|
|
42
|
+
/** Track dot style override. */
|
|
43
|
+
trackDotStyle?: StyleProp<ViewStyle>
|
|
44
|
+
/** Progress dot style override. */
|
|
45
|
+
progressDotStyle?: StyleProp<ViewStyle>
|
|
46
|
+
/** Center content style override. */
|
|
47
|
+
contentStyle?: StyleProp<ViewStyle>
|
|
48
|
+
/** Score tier stack style override. */
|
|
49
|
+
scoreTierStyle?: StyleProp<ViewStyle>
|
|
50
|
+
/** Score trend row style override. */
|
|
51
|
+
scoreTrendStyle?: StyleProp<ViewStyle>
|
|
52
|
+
/** Label text style override. */
|
|
53
|
+
labelStyle?: StyleProp<TextStyle>
|
|
54
|
+
/** Score text style override. */
|
|
55
|
+
scoreLabelStyle?: StyleProp<TextStyle>
|
|
56
|
+
/** Tier text style override. */
|
|
57
|
+
tierLabelStyle?: StyleProp<TextStyle>
|
|
58
|
+
/** Accessibility label for the whole progress component. */
|
|
59
|
+
accessibilityLabel?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_DOT_COUNT = 24
|
|
63
|
+
const START_ANGLE_DEGREES = -90
|
|
64
|
+
|
|
65
|
+
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
|
|
66
|
+
|
|
67
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
68
|
+
if (typeof value === 'number') {
|
|
69
|
+
return Number.isFinite(value) ? value : fallback
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
const parsed = Number(value)
|
|
74
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return fallback
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
|
|
81
|
+
if (typeof value === 'number') {
|
|
82
|
+
return String(value) as TextStyle['fontWeight']
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return value as TextStyle['fontWeight']
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return fallback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const getBackgroundColor = (style: StyleProp<ViewStyle>, fallback: string) => {
|
|
93
|
+
const flattened = StyleSheet.flatten(style)
|
|
94
|
+
return typeof flattened.backgroundColor === 'string'
|
|
95
|
+
? flattened.backgroundColor
|
|
96
|
+
: fallback
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function CircularProgressBarDoted({
|
|
100
|
+
value = 72,
|
|
101
|
+
dotCount = DEFAULT_DOT_COUNT,
|
|
102
|
+
label = 'Rating',
|
|
103
|
+
tierLabel = 'Doing great',
|
|
104
|
+
showChevron = true,
|
|
105
|
+
onPress,
|
|
106
|
+
onTierPress,
|
|
107
|
+
modes: propModes = EMPTY_MODES,
|
|
108
|
+
children,
|
|
109
|
+
style,
|
|
110
|
+
ringStyle,
|
|
111
|
+
trackDotStyle,
|
|
112
|
+
progressDotStyle,
|
|
113
|
+
contentStyle,
|
|
114
|
+
scoreTierStyle,
|
|
115
|
+
scoreTrendStyle,
|
|
116
|
+
labelStyle,
|
|
117
|
+
scoreLabelStyle,
|
|
118
|
+
tierLabelStyle,
|
|
119
|
+
accessibilityLabel,
|
|
120
|
+
onLayout,
|
|
121
|
+
...rest
|
|
122
|
+
}: CircularProgressBarDotedProps) {
|
|
123
|
+
const { modes: globalModes } = useTokens()
|
|
124
|
+
const modes = { ...globalModes, ...propModes }
|
|
125
|
+
const [layoutSize, setLayoutSize] = useState(0)
|
|
126
|
+
|
|
127
|
+
const normalizedValue = clamp(value, 0, 100)
|
|
128
|
+
const resolvedDotCount = Math.max(1, Math.floor(dotCount))
|
|
129
|
+
const activeDots = normalizedValue <= 0
|
|
130
|
+
? 0
|
|
131
|
+
: Math.ceil((normalizedValue / 100) * resolvedDotCount)
|
|
132
|
+
|
|
133
|
+
const dotShadowSize = toNumber(getVariableByName('circularProgressBarDoted/dot/shadow/size', modes), 6)
|
|
134
|
+
const baseDotSize = toNumber(getVariableByName('circularProgressBarDoted/dot/size', modes), 6)
|
|
135
|
+
const dotSize = baseDotSize + dotShadowSize
|
|
136
|
+
const outerDotSize = dotSize
|
|
137
|
+
const ringSize = layoutSize
|
|
138
|
+
const ringRadius = Math.max(0, (ringSize - outerDotSize) / 2)
|
|
139
|
+
|
|
140
|
+
const trackDotColor = getBackgroundColor(
|
|
141
|
+
trackDotStyle,
|
|
142
|
+
getVariableByName('circularProgressBarDoted/trackDot/bg', modes) as string || '#ebebed'
|
|
143
|
+
)
|
|
144
|
+
const progressDotColor = getBackgroundColor(
|
|
145
|
+
progressDotStyle,
|
|
146
|
+
getVariableByName('circularProgressBarDoted/progressDot/bg', modes) as string || '#25ab21'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const contentGap = toNumber(getVariableByName('circularProgressBarDoted/gap', modes), 12)
|
|
150
|
+
const scoreTierGap = toNumber(getVariableByName('circularProgressBarDoted/scoreTier/gap', modes), 6)
|
|
151
|
+
const scoreTierWidth = toNumber(getVariableByName('circularProgressBarDoted/scoreTier/width', modes), 116)
|
|
152
|
+
const scoreTrendGap = toNumber(getVariableByName('circularProgressBarDoted/scoreTrend/gap', modes), 2)
|
|
153
|
+
const scoreTrendHeight = toNumber(getVariableByName('circularProgressBarDoted/scoreTrend/height', modes), 24)
|
|
154
|
+
|
|
155
|
+
const typographyFontFamily = getVariableByName('Typography/Font Family', modes) as string || 'JioType Var'
|
|
156
|
+
const labelColor = getVariableByName('circularProgressBarDoted/label/color', modes) as string || '#080d1a'
|
|
157
|
+
const labelFontSize = toNumber(getVariableByName('Typography/Size/Body/XS', modes), 12)
|
|
158
|
+
const labelFontWeight = toFontWeight(getVariableByName('Typography/Font Weight/Body Low (Regular)', modes), '400')
|
|
159
|
+
|
|
160
|
+
const scoreColor = getVariableByName('circularProgressBarDoted/scoreLabel/color', modes) as string || '#080d1a'
|
|
161
|
+
const scoreFontSize = toNumber(getVariableByName('circularProgressBarDoted/scoreLabel/fontSize', modes), 56)
|
|
162
|
+
const scoreFontFamily = getVariableByName('circularProgressBarDoted/scoreLabel/fontFamily', modes) as string || 'JioType Var'
|
|
163
|
+
const scoreLineHeight = toNumber(getVariableByName('circularProgressBarDoted/scoreLabel/lineHeight', modes), 56)
|
|
164
|
+
const scoreFontWeight = toFontWeight(getVariableByName('circularProgressBarDoted/scoreLabel/fontWeight', modes), '900')
|
|
165
|
+
|
|
166
|
+
const tierColor = getVariableByName('circularProgressBarDoted/tierLabel/color', modes) as string || '#080d1a'
|
|
167
|
+
const tierFontSize = toNumber(getVariableByName('Typography/Size/Body/M', modes), 16)
|
|
168
|
+
const tierFontWeight = toFontWeight(getVariableByName('Typography/Font Weight/Body High', modes), '700')
|
|
169
|
+
const iconColor = getVariableByName('circularProgressBarDoted/icon/color', modes) as string || '#303338'
|
|
170
|
+
const iconSize = toNumber(getVariableByName('circularProgressBarDoted/icon/size', modes), 24)
|
|
171
|
+
|
|
172
|
+
const dots = useMemo(
|
|
173
|
+
() => Array.from({ length: resolvedDotCount }, (_, index) => {
|
|
174
|
+
const angle = ((360 / resolvedDotCount) * index + START_ANGLE_DEGREES) * (Math.PI / 180)
|
|
175
|
+
const center = ringSize / 2
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
isActive: index < activeDots,
|
|
179
|
+
left: center + ringRadius * Math.cos(angle) - outerDotSize / 2,
|
|
180
|
+
top: center + ringRadius * Math.sin(angle) - outerDotSize / 2,
|
|
181
|
+
}
|
|
182
|
+
}),
|
|
183
|
+
[activeDots, outerDotSize, resolvedDotCount, ringRadius, ringSize]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const containerStyle: ViewStyle = {
|
|
187
|
+
alignItems: 'center',
|
|
188
|
+
alignSelf: 'stretch',
|
|
189
|
+
height: '100%',
|
|
190
|
+
justifyContent: 'center',
|
|
191
|
+
position: 'relative',
|
|
192
|
+
width: '100%',
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const computedRingStyle: ViewStyle = {
|
|
196
|
+
height: ringSize,
|
|
197
|
+
position: 'absolute',
|
|
198
|
+
width: ringSize,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const dotOuterStyle: ViewStyle = {
|
|
202
|
+
alignItems: 'center',
|
|
203
|
+
height: outerDotSize,
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
position: 'absolute',
|
|
206
|
+
width: outerDotSize,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const dotInnerStyle: ViewStyle = {
|
|
210
|
+
borderRadius: dotSize / 2,
|
|
211
|
+
height: dotSize,
|
|
212
|
+
width: dotSize,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const computedContentStyle: ViewStyle = {
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
gap: contentGap,
|
|
218
|
+
justifyContent: 'center',
|
|
219
|
+
minWidth: scoreTierWidth,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const computedScoreTierStyle: ViewStyle = {
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
gap: scoreTierGap,
|
|
225
|
+
justifyContent: 'center',
|
|
226
|
+
width: '100%',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const computedScoreTrendStyle: ViewStyle = {
|
|
230
|
+
alignItems: 'center',
|
|
231
|
+
flexDirection: 'row',
|
|
232
|
+
gap: scoreTrendGap,
|
|
233
|
+
height: scoreTrendHeight,
|
|
234
|
+
justifyContent: 'center',
|
|
235
|
+
minWidth: scoreTierWidth,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const computedLabelStyle: TextStyle = {
|
|
239
|
+
color: labelColor,
|
|
240
|
+
fontFamily: typographyFontFamily,
|
|
241
|
+
fontSize: labelFontSize,
|
|
242
|
+
fontWeight: labelFontWeight,
|
|
243
|
+
lineHeight: labelFontSize * 1.3,
|
|
244
|
+
textAlign: 'center',
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const computedScoreLabelStyle: TextStyle = {
|
|
248
|
+
color: scoreColor,
|
|
249
|
+
fontFamily: scoreFontFamily,
|
|
250
|
+
fontSize: scoreFontSize,
|
|
251
|
+
fontWeight: scoreFontWeight,
|
|
252
|
+
lineHeight: scoreLineHeight,
|
|
253
|
+
textAlign: 'center',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const computedTierLabelStyle: TextStyle = {
|
|
257
|
+
color: tierColor,
|
|
258
|
+
fontFamily: typographyFontFamily,
|
|
259
|
+
fontSize: tierFontSize,
|
|
260
|
+
fontWeight: tierFontWeight,
|
|
261
|
+
lineHeight: tierFontSize * 1.3,
|
|
262
|
+
textAlign: 'center',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const resolvedScoreLabel = String(Math.round(normalizedValue))
|
|
266
|
+
const defaultAccessibilityLabel =
|
|
267
|
+
accessibilityLabel ?? `${label}. ${resolvedScoreLabel} out of 100. ${tierLabel}`
|
|
268
|
+
|
|
269
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
270
|
+
onLayout?.(event)
|
|
271
|
+
|
|
272
|
+
const { width, height } = event.nativeEvent.layout
|
|
273
|
+
const nextSize = Math.max(0, Math.min(width, height))
|
|
274
|
+
setLayoutSize((currentSize) => currentSize === nextSize ? currentSize : nextSize)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const trendContent = (
|
|
278
|
+
<>
|
|
279
|
+
<Text numberOfLines={1} style={[computedTierLabelStyle, tierLabelStyle]}>
|
|
280
|
+
{tierLabel}
|
|
281
|
+
</Text>
|
|
282
|
+
{showChevron ? (
|
|
283
|
+
<IconChevronright
|
|
284
|
+
width={iconSize}
|
|
285
|
+
height={iconSize}
|
|
286
|
+
fill={iconColor}
|
|
287
|
+
color={iconColor}
|
|
288
|
+
/>
|
|
289
|
+
) : null}
|
|
290
|
+
</>
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<Pressable
|
|
295
|
+
accessibilityRole="progressbar"
|
|
296
|
+
accessibilityLabel={defaultAccessibilityLabel}
|
|
297
|
+
accessibilityValue={{ min: 0, max: 100, now: normalizedValue }}
|
|
298
|
+
disabled={!onPress}
|
|
299
|
+
onLayout={handleLayout}
|
|
300
|
+
onPress={onPress}
|
|
301
|
+
style={[containerStyle, style]}
|
|
302
|
+
{...rest}
|
|
303
|
+
>
|
|
304
|
+
<View pointerEvents="none" style={[computedRingStyle, ringStyle]}>
|
|
305
|
+
{dots.map((dot, index) => (
|
|
306
|
+
<View
|
|
307
|
+
key={index}
|
|
308
|
+
style={[
|
|
309
|
+
dotOuterStyle,
|
|
310
|
+
{ left: dot.left, top: dot.top },
|
|
311
|
+
]}
|
|
312
|
+
>
|
|
313
|
+
<View
|
|
314
|
+
style={[
|
|
315
|
+
dotInnerStyle,
|
|
316
|
+
{ backgroundColor: dot.isActive ? progressDotColor : trackDotColor },
|
|
317
|
+
dot.isActive ? progressDotStyle : trackDotStyle,
|
|
318
|
+
]}
|
|
319
|
+
/>
|
|
320
|
+
</View>
|
|
321
|
+
))}
|
|
322
|
+
</View>
|
|
323
|
+
|
|
324
|
+
<View style={[computedContentStyle, contentStyle]}>
|
|
325
|
+
{children ? (
|
|
326
|
+
cloneChildrenWithModes(children, modes)
|
|
327
|
+
) : (
|
|
328
|
+
<>
|
|
329
|
+
<View style={[computedScoreTierStyle, scoreTierStyle]}>
|
|
330
|
+
<Text style={[computedLabelStyle, labelStyle]}>
|
|
331
|
+
{label}
|
|
332
|
+
</Text>
|
|
333
|
+
<Text style={[computedScoreLabelStyle, scoreLabelStyle]}>
|
|
334
|
+
{resolvedScoreLabel}
|
|
335
|
+
</Text>
|
|
336
|
+
</View>
|
|
337
|
+
{onTierPress ? (
|
|
338
|
+
<Pressable
|
|
339
|
+
accessibilityRole="button"
|
|
340
|
+
onPress={onTierPress}
|
|
341
|
+
style={[computedScoreTrendStyle, scoreTrendStyle]}
|
|
342
|
+
>
|
|
343
|
+
{trendContent}
|
|
344
|
+
</Pressable>
|
|
345
|
+
) : (
|
|
346
|
+
<View style={[computedScoreTrendStyle, scoreTrendStyle]}>
|
|
347
|
+
{trendContent}
|
|
348
|
+
</View>
|
|
349
|
+
)}
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</View>
|
|
353
|
+
</Pressable>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export default CircularProgressBarDoted
|
|
@@ -0,0 +1,241 @@
|
|
|
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 CircularProgressBarDoted from '../CircularProgressBarDoted/CircularProgressBarDoted'
|
|
14
|
+
import Nudge from '../Nudge/Nudge'
|
|
15
|
+
|
|
16
|
+
type CircularRatingBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
|
|
17
|
+
|
|
18
|
+
export type CircularRatingProps = CircularRatingBaseProps & {
|
|
19
|
+
/** Rating value. Clamped by CircularProgressBarDoted between 0 and 100. */
|
|
20
|
+
value?: number
|
|
21
|
+
/** Number of dots rendered around the rating ring. */
|
|
22
|
+
dotCount?: number
|
|
23
|
+
/** Small label above the score. */
|
|
24
|
+
label?: string
|
|
25
|
+
/** Text below the score. */
|
|
26
|
+
tierLabel?: string
|
|
27
|
+
/** Footer timestamp/copy shown below the rating. */
|
|
28
|
+
footerText?: string
|
|
29
|
+
/** Show the footer info icon. */
|
|
30
|
+
showFooterIcon?: boolean
|
|
31
|
+
/** Show the bottom inline nudge. */
|
|
32
|
+
showNudge?: boolean
|
|
33
|
+
/** Body text for the default bottom nudge. */
|
|
34
|
+
nudgeBody?: string
|
|
35
|
+
/** Button label for the default bottom nudge. */
|
|
36
|
+
nudgeButtonLabel?: string
|
|
37
|
+
/** Called when the nudge button is pressed. */
|
|
38
|
+
onPressNudgeButton?: () => void
|
|
39
|
+
/** Called when the rating tier row is pressed. */
|
|
40
|
+
onTierPress?: () => void
|
|
41
|
+
/** Optional footer slot. Receives `modes` recursively. */
|
|
42
|
+
footerSlot?: React.ReactNode
|
|
43
|
+
/** Optional nudge slot. Receives `modes` recursively. */
|
|
44
|
+
nudgeSlot?: React.ReactNode
|
|
45
|
+
/** Design token modes forwarded to token lookups and child components. */
|
|
46
|
+
modes?: Record<string, any>
|
|
47
|
+
/** Optional container style overrides. */
|
|
48
|
+
style?: StyleProp<ViewStyle>
|
|
49
|
+
/** Optional rating ring wrapper style overrides. */
|
|
50
|
+
ratingStyle?: StyleProp<ViewStyle>
|
|
51
|
+
/** Optional footer row style overrides. */
|
|
52
|
+
footerStyle?: StyleProp<ViewStyle>
|
|
53
|
+
/** Optional footer text style overrides. */
|
|
54
|
+
footerTextStyle?: StyleProp<TextStyle>
|
|
55
|
+
/** Optional nudge style overrides. */
|
|
56
|
+
nudgeStyle?: StyleProp<ViewStyle>
|
|
57
|
+
/** Accessibility label for the whole component. */
|
|
58
|
+
accessibilityLabel?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CircularRatingTokens {
|
|
62
|
+
containerStyle: ViewStyle
|
|
63
|
+
ratingStyle: ViewStyle
|
|
64
|
+
footerStyle: ViewStyle
|
|
65
|
+
footerTextStyle: TextStyle
|
|
66
|
+
footerIconColor: string
|
|
67
|
+
footerIconSize: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
71
|
+
if (typeof value === 'number') {
|
|
72
|
+
return Number.isFinite(value) ? value : fallback
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
const parsed = Number(value)
|
|
77
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return fallback
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
|
|
84
|
+
if (typeof value === 'number') {
|
|
85
|
+
return String(value) as TextStyle['fontWeight']
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof value === 'string') {
|
|
89
|
+
return value as TextStyle['fontWeight']
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return fallback
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveCircularRatingTokens(modes: Record<string, any>): CircularRatingTokens {
|
|
96
|
+
const gap = toNumber(getVariableByName('circularRating/gap', modes), 32)
|
|
97
|
+
const padding = toNumber(getVariableByName('circularRating/padding', modes), 10)
|
|
98
|
+
|
|
99
|
+
const footerGap = toNumber(getVariableByName('circularRating/footer/gap', modes), 4)
|
|
100
|
+
const footerHeight = toNumber(getVariableByName('circularRating/footer/height', modes), 16)
|
|
101
|
+
const footerTextColor = getVariableByName('circularRating/footer/text/color', modes) || '#0d0d0f'
|
|
102
|
+
const footerTextFontSize = toNumber(getVariableByName('circularRating/footer/text/fontSize', modes), 12)
|
|
103
|
+
const footerTextFontFamily = getVariableByName('circularRating/footer/text/fontFamily', modes) || 'JioType Var'
|
|
104
|
+
const footerTextLineHeight = toNumber(getVariableByName('circularRating/footer/text/lineHeight', modes), 16)
|
|
105
|
+
const footerTextFontWeight = toFontWeight(
|
|
106
|
+
getVariableByName('circularRating/footer/text/fontWeight', modes),
|
|
107
|
+
'400'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
containerStyle: {
|
|
112
|
+
alignItems: 'flex-start',
|
|
113
|
+
gap,
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
padding,
|
|
116
|
+
},
|
|
117
|
+
ratingStyle: {
|
|
118
|
+
height: 320,
|
|
119
|
+
width: 320,
|
|
120
|
+
},
|
|
121
|
+
footerStyle: {
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
flexDirection: 'row',
|
|
124
|
+
gap: footerGap,
|
|
125
|
+
height: footerHeight,
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
width: '100%',
|
|
128
|
+
},
|
|
129
|
+
footerTextStyle: {
|
|
130
|
+
color: footerTextColor as string,
|
|
131
|
+
fontFamily: footerTextFontFamily as string,
|
|
132
|
+
fontSize: footerTextFontSize,
|
|
133
|
+
fontWeight: footerTextFontWeight,
|
|
134
|
+
lineHeight: footerTextLineHeight,
|
|
135
|
+
textAlign: 'center',
|
|
136
|
+
},
|
|
137
|
+
footerIconColor: (getVariableByName('circularRating/footer/icon/color', modes) || '#1a1c1f') as string,
|
|
138
|
+
footerIconSize: toNumber(getVariableByName('circularRating/footer/icon/size', modes), 16),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function CircularRating({
|
|
143
|
+
value = 72,
|
|
144
|
+
dotCount = 24,
|
|
145
|
+
label = 'Rating',
|
|
146
|
+
tierLabel = 'Doing great',
|
|
147
|
+
footerText = 'Updated on 1 March',
|
|
148
|
+
showFooterIcon = true,
|
|
149
|
+
showNudge = true,
|
|
150
|
+
nudgeBody = 'Split this transaction into installments',
|
|
151
|
+
nudgeButtonLabel = 'Button',
|
|
152
|
+
onPressNudgeButton,
|
|
153
|
+
onTierPress,
|
|
154
|
+
footerSlot,
|
|
155
|
+
nudgeSlot,
|
|
156
|
+
modes: propModes = EMPTY_MODES,
|
|
157
|
+
style,
|
|
158
|
+
ratingStyle,
|
|
159
|
+
footerStyle,
|
|
160
|
+
footerTextStyle,
|
|
161
|
+
nudgeStyle,
|
|
162
|
+
accessibilityLabel,
|
|
163
|
+
...rest
|
|
164
|
+
}: CircularRatingProps) {
|
|
165
|
+
const { modes: globalModes } = useTokens()
|
|
166
|
+
const modes = useMemo(
|
|
167
|
+
() => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
168
|
+
? EMPTY_MODES
|
|
169
|
+
: { ...globalModes, ...propModes }),
|
|
170
|
+
[globalModes, propModes]
|
|
171
|
+
)
|
|
172
|
+
const tokens = useMemo(() => resolveCircularRatingTokens(modes), [modes])
|
|
173
|
+
|
|
174
|
+
const processedFooterSlot = useMemo(() => {
|
|
175
|
+
if (!footerSlot) return null
|
|
176
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(footerSlot), modes)
|
|
177
|
+
return processed.length === 1 ? processed[0] : processed
|
|
178
|
+
}, [footerSlot, modes])
|
|
179
|
+
|
|
180
|
+
const processedNudgeSlot = useMemo(() => {
|
|
181
|
+
if (!nudgeSlot) return null
|
|
182
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(nudgeSlot), modes)
|
|
183
|
+
return processed.length === 1 ? processed[0] : processed
|
|
184
|
+
}, [nudgeSlot, modes])
|
|
185
|
+
|
|
186
|
+
const defaultAccessibilityLabel =
|
|
187
|
+
accessibilityLabel ?? `${label}. ${Math.round(value)} out of 100. ${tierLabel}. ${footerText}`
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<View
|
|
191
|
+
accessibilityLabel={defaultAccessibilityLabel}
|
|
192
|
+
style={[tokens.containerStyle, style]}
|
|
193
|
+
{...rest}
|
|
194
|
+
>
|
|
195
|
+
<View style={[tokens.ratingStyle, ratingStyle]}>
|
|
196
|
+
<CircularProgressBarDoted
|
|
197
|
+
value={value}
|
|
198
|
+
dotCount={dotCount}
|
|
199
|
+
label={label}
|
|
200
|
+
tierLabel={tierLabel}
|
|
201
|
+
onTierPress={onTierPress}
|
|
202
|
+
modes={modes}
|
|
203
|
+
/>
|
|
204
|
+
</View>
|
|
205
|
+
|
|
206
|
+
<View style={[tokens.footerStyle, footerStyle]}>
|
|
207
|
+
{processedFooterSlot || (
|
|
208
|
+
<>
|
|
209
|
+
<Text numberOfLines={1} style={[tokens.footerTextStyle, footerTextStyle]}>
|
|
210
|
+
{footerText}
|
|
211
|
+
</Text>
|
|
212
|
+
{showFooterIcon ? (
|
|
213
|
+
<Icon
|
|
214
|
+
name="ic_info"
|
|
215
|
+
size={tokens.footerIconSize}
|
|
216
|
+
color={tokens.footerIconColor}
|
|
217
|
+
accessibilityElementsHidden={true}
|
|
218
|
+
importantForAccessibility="no"
|
|
219
|
+
/>
|
|
220
|
+
) : null}
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
</View>
|
|
224
|
+
|
|
225
|
+
{showNudge ? (
|
|
226
|
+
processedNudgeSlot || (
|
|
227
|
+
<Nudge
|
|
228
|
+
type="inline-compact"
|
|
229
|
+
body={nudgeBody}
|
|
230
|
+
buttonLabel={nudgeButtonLabel}
|
|
231
|
+
onPressButton={onPressNudgeButton}
|
|
232
|
+
modes={modes}
|
|
233
|
+
style={[{ width: 312 }, nudgeStyle]}
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
236
|
+
) : null}
|
|
237
|
+
</View>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default React.memo(CircularRating)
|