jfs-components 0.0.71 → 0.0.73

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 (141) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  4. package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +2 -2
  5. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +213 -0
  6. package/lib/commonjs/components/CardFinancialCondition/CardFinancialCondition.js +213 -0
  7. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  8. package/lib/commonjs/components/Carousel/Carousel.js +9 -7
  9. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  10. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +125 -0
  11. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  12. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  13. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  14. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  15. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  16. package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -2
  17. package/lib/commonjs/components/InstitutionBadge/InstitutionBadge.js +132 -0
  18. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  19. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  20. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  21. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  22. package/lib/commonjs/components/OTP/OTP.js +381 -37
  23. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  24. package/lib/commonjs/components/Radio/Radio.js +194 -0
  25. package/lib/commonjs/components/RadioButton/RadioButton.js +21 -188
  26. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  27. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  28. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  29. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  30. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  31. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  32. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  33. package/lib/commonjs/components/index.js +192 -1
  34. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  35. package/lib/commonjs/icons/registry.js +1 -1
  36. package/lib/commonjs/utils/index.js +7 -0
  37. package/lib/commonjs/utils/number-utils.js +57 -0
  38. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  39. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  40. package/lib/module/components/CardAdvisory/CardAdvisory.js +2 -2
  41. package/lib/module/components/CardBankAccount/CardBankAccount.js +208 -0
  42. package/lib/module/components/CardFinancialCondition/CardFinancialCondition.js +207 -0
  43. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  44. package/lib/module/components/Carousel/Carousel.js +9 -7
  45. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  46. package/lib/module/components/CheckboxItem/CheckboxItem.js +119 -0
  47. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  48. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  49. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  50. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  51. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  52. package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -2
  53. package/lib/module/components/InstitutionBadge/InstitutionBadge.js +127 -0
  54. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  55. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  56. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  57. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  58. package/lib/module/components/OTP/OTP.js +381 -38
  59. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  60. package/lib/module/components/Radio/Radio.js +188 -0
  61. package/lib/module/components/RadioButton/RadioButton.js +20 -185
  62. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  63. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  64. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  65. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  66. package/lib/module/components/StatItem/StatItem.js +66 -36
  67. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  68. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  69. package/lib/module/components/index.js +28 -1
  70. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  71. package/lib/module/icons/registry.js +1 -1
  72. package/lib/module/utils/index.js +2 -1
  73. package/lib/module/utils/number-utils.js +53 -0
  74. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  75. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  76. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +79 -0
  77. package/lib/typescript/src/components/CardFinancialCondition/CardFinancialCondition.d.ts +50 -0
  78. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  79. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  80. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +56 -0
  81. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  82. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  83. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  84. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  85. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  86. package/lib/typescript/src/components/InstitutionBadge/InstitutionBadge.d.ts +30 -0
  87. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  88. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  89. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  90. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  91. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  92. package/lib/typescript/src/components/Radio/Radio.d.ts +30 -0
  93. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +20 -28
  94. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  95. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  96. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  97. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  98. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  99. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  100. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  101. package/lib/typescript/src/components/index.d.ts +29 -2
  102. package/lib/typescript/src/icons/registry.d.ts +1 -1
  103. package/lib/typescript/src/utils/index.d.ts +1 -0
  104. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  105. package/package.json +1 -1
  106. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  107. package/src/components/BrandChip/BrandChip.tsx +235 -0
  108. package/src/components/CardAdvisory/CardAdvisory.tsx +2 -2
  109. package/src/components/CardBankAccount/CardBankAccount.tsx +295 -0
  110. package/src/components/CardFinancialCondition/CardFinancialCondition.tsx +366 -0
  111. package/src/components/CardInsight/CardInsight.tsx +239 -0
  112. package/src/components/Carousel/Carousel.tsx +14 -6
  113. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  114. package/src/components/CheckboxItem/CheckboxItem.tsx +174 -0
  115. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  116. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  117. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  118. package/src/components/DonutChart/DonutChart.tsx +503 -0
  119. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  120. package/src/components/HoldingsCard/HoldingsCard.tsx +2 -2
  121. package/src/components/InstitutionBadge/InstitutionBadge.tsx +216 -0
  122. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  123. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  124. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  125. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  126. package/src/components/OTP/OTP.tsx +476 -29
  127. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  128. package/src/components/Radio/Radio.tsx +227 -0
  129. package/src/components/RadioButton/RadioButton.tsx +23 -225
  130. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  131. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  132. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  133. package/src/components/StatGroup/StatGroup.tsx +169 -0
  134. package/src/components/StatItem/StatItem.tsx +117 -40
  135. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  136. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  137. package/src/components/index.ts +39 -2
  138. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  139. package/src/icons/registry.ts +1 -1
  140. package/src/utils/index.ts +1 -0
  141. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,378 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type LayoutChangeEvent,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import Svg, { Rect } from 'react-native-svg'
11
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
13
+ import { EMPTY_MODES } from '../../utils/react-utils'
14
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
15
+
16
+ /**
17
+ * One entry in the {@link CoverageBarComparisonProps.bars} array.
18
+ *
19
+ * Each entry carries **both** the bar's data and its legend label, so the
20
+ * number of bars is intrinsically equal to the number of legend items.
21
+ * There is no separate `legends` prop — that is by design.
22
+ */
23
+ export type CoverageBarComparisonItem = {
24
+ /** Stable React key. */
25
+ key?: React.Key
26
+ /**
27
+ * Numeric value used to scale the bar height proportionally. Bars share
28
+ * the chart area; the largest value (or {@link CoverageBarComparisonProps.max})
29
+ * fills the full bar area, smaller bars are proportionally shorter.
30
+ */
31
+ value: number
32
+ /**
33
+ * Text rendered above the bar (the Figma `Header` slot, e.g. `"₹1L (40%)"`).
34
+ * Omit to hide the header for this bar.
35
+ */
36
+ label?: React.ReactNode
37
+ /**
38
+ * Legend label rendered below the chart for this bar. Required: the legend
39
+ * row always has exactly one item per bar.
40
+ */
41
+ legend: React.ReactNode
42
+ /**
43
+ * Optional value text shown on the right of the legend (e.g. `"₹12,400"`).
44
+ * When omitted the legend renders only the colored dot + label.
45
+ */
46
+ legendValue?: React.ReactNode
47
+ /**
48
+ * Hard-override the bar fill color. Bypasses `valueBar/bar/background`
49
+ * resolution entirely — and is also used for the legend indicator dot, so
50
+ * the bar and its legend stay in sync.
51
+ */
52
+ color?: string
53
+ /**
54
+ * Per-bar design token mode overrides. Merged on top of the parent `modes`
55
+ * and the per-index `Emphasis / DataViz` defaults injected by the parent.
56
+ */
57
+ modes?: Record<string, any>
58
+ /** Per-bar accessibility label. */
59
+ accessibilityLabel?: string
60
+ }
61
+
62
+ export type CoverageBarComparisonProps = {
63
+ /**
64
+ * The bars to render. Each entry contains the value, the bar header label
65
+ * **and** the legend label, so the chart and the legend can never go out of
66
+ * sync. Defaults to a 2-item "Current vs Recommended" example matching the
67
+ * Figma reference.
68
+ */
69
+ bars?: CoverageBarComparisonItem[]
70
+ /**
71
+ * Maximum value used to scale the bars. Defaults to the largest
72
+ * `bars[i].value`. Pass an explicit `max` when bars represent absolute
73
+ * numbers and you want a fixed scale (e.g. `max=100` for percentages).
74
+ */
75
+ max?: number
76
+ /**
77
+ * Total height of the bar visualization area in px (includes the per-bar
78
+ * header label space). The largest bar fills `height - labelHeight - gap`.
79
+ * Default: `100`.
80
+ */
81
+ height?: number
82
+ /**
83
+ * Vertical gap (px) between the chart and the legend row. Default: `12`,
84
+ * matching the Figma reference.
85
+ */
86
+ legendGap?: number
87
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
88
+ modes?: Record<string, any>
89
+ /** Container style override. */
90
+ style?: StyleProp<ViewStyle>
91
+ /** Style applied to the chart row (the bars container). */
92
+ chartStyle?: StyleProp<ViewStyle>
93
+ /** Style applied to the legend row. */
94
+ legendStyle?: StyleProp<ViewStyle>
95
+ /** Style applied to every value-label text above the bars. */
96
+ labelStyle?: StyleProp<TextStyle>
97
+ /** Accessibility label for the entire component. */
98
+ accessibilityLabel?: string
99
+ }
100
+
101
+ /**
102
+ * Default per-index `Emphasis / DataViz` modes applied when the caller does
103
+ * not provide its own override. Ascends Low → Medium → High so the natural
104
+ * "Current vs Recommended" two-bar story matches the Figma reference (left
105
+ * = lighter / Low, right = darker / High). Cycles for >3 bars.
106
+ */
107
+ const DEFAULT_EMPHASIS_CYCLE = ['Low', 'Medium', 'High'] as const
108
+
109
+ function defaultEmphasisFor(index: number, total: number) {
110
+ if (total <= 1) {
111
+ return 'High'
112
+ }
113
+ if (total === 2) {
114
+ return index === 0 ? 'Low' : 'High'
115
+ }
116
+ return DEFAULT_EMPHASIS_CYCLE[index % DEFAULT_EMPHASIS_CYCLE.length]
117
+ }
118
+
119
+ const DEFAULT_BARS: CoverageBarComparisonItem[] = [
120
+ { value: 40, label: '₹1L (40%)', legend: 'Current coverage' },
121
+ { value: 100, label: '₹2.5L (100%)', legend: 'Recommended coverage' },
122
+ ]
123
+
124
+ const toNumber = (value: unknown, fallback: number) => {
125
+ if (typeof value === 'number') {
126
+ return Number.isFinite(value) ? value : fallback
127
+ }
128
+ if (typeof value === 'string') {
129
+ const parsed = Number(value)
130
+ return Number.isFinite(parsed) ? parsed : fallback
131
+ }
132
+ return fallback
133
+ }
134
+
135
+ const toFontWeight = (
136
+ value: unknown,
137
+ fallback: TextStyle['fontWeight']
138
+ ): TextStyle['fontWeight'] => {
139
+ if (typeof value === 'number') {
140
+ return String(value) as TextStyle['fontWeight']
141
+ }
142
+ if (typeof value === 'string') {
143
+ return value as TextStyle['fontWeight']
144
+ }
145
+ return fallback
146
+ }
147
+
148
+ type BarShapeProps = {
149
+ height: number
150
+ color: string
151
+ radius: number
152
+ }
153
+
154
+ /**
155
+ * Bar shape rendered with `react-native-svg`. The wrapper measures its width
156
+ * via `onLayout` so the SVG can use concrete dimensions (RN SVG does not
157
+ * resolve percentage shape attributes when the parent SVG itself has a
158
+ * percentage `width`).
159
+ */
160
+ function BarShape({ height, color, radius }: BarShapeProps) {
161
+ const [width, setWidth] = React.useState(0)
162
+ const handleLayout = React.useCallback((event: LayoutChangeEvent) => {
163
+ const next = event.nativeEvent.layout.width
164
+ setWidth((prev) => (prev === next ? prev : next))
165
+ }, [])
166
+
167
+ if (height <= 0) {
168
+ return <View onLayout={handleLayout} style={{ width: '100%', height: 0 }} />
169
+ }
170
+
171
+ const safeRadius = Math.max(0, Math.min(radius, width / 2, height / 2))
172
+
173
+ return (
174
+ <View
175
+ onLayout={handleLayout}
176
+ style={{ width: '100%', height, minWidth: 1 }}
177
+ >
178
+ {width > 0 ? (
179
+ <Svg width={width} height={height}>
180
+ <Rect
181
+ x={0}
182
+ y={0}
183
+ width={width}
184
+ height={height}
185
+ rx={safeRadius}
186
+ ry={safeRadius}
187
+ fill={color}
188
+ />
189
+ </Svg>
190
+ ) : null}
191
+ </View>
192
+ )
193
+ }
194
+
195
+ /**
196
+ * `CoverageBarComparison` renders a small vertical-bar chart that compares
197
+ * 2+ values side by side, with a legend row directly below where each item
198
+ * is intrinsically tied to one bar.
199
+ *
200
+ * Cohesiveness guarantees:
201
+ * - The legend row is **derived** from the same `bars` prop, so the count
202
+ * and order can never desynchronise from the chart.
203
+ * - Each bar's color, its tokenized `Emphasis / DataViz` mode and its
204
+ * legend indicator dot are the same value.
205
+ *
206
+ * Bars are drawn with `react-native-svg` (`<Rect>` with `rx`), and the
207
+ * fonts/spacing/colors are sourced from the Figma `valueBar/*` and
208
+ * `coverageBarComparison/*` tokens.
209
+ *
210
+ * @component
211
+ */
212
+ function CoverageBarComparison({
213
+ bars = DEFAULT_BARS,
214
+ max,
215
+ height = 100,
216
+ legendGap = 12,
217
+ modes: propModes = EMPTY_MODES,
218
+ style,
219
+ chartStyle,
220
+ legendStyle,
221
+ labelStyle,
222
+ accessibilityLabel,
223
+ }: CoverageBarComparisonProps) {
224
+ const { modes: globalModes } = useTokens()
225
+ const modes = React.useMemo(
226
+ () => ({ ...globalModes, ...propModes }),
227
+ [globalModes, propModes]
228
+ )
229
+
230
+ const wrapGap = toNumber(getVariableByName('coverageBarComparison/wrap/gap', modes), 6)
231
+ const valueBarGap = toNumber(getVariableByName('valueBar/gap', modes), 4)
232
+ const barRadius = toNumber(getVariableByName('valueBar/bar/radius', modes), 10)
233
+
234
+ const fontFamily =
235
+ (getVariableByName('valueBar/fontFamily', modes) as string | null) ?? 'JioType Var'
236
+ const fontSize = toNumber(getVariableByName('valueBar/fontSize', modes), 12)
237
+ const lineHeight = toNumber(getVariableByName('valueBar/lineHeight', modes), 16)
238
+ const fontWeight = toFontWeight(getVariableByName('valueBar/fontWeight', modes), '700')
239
+ const foreground =
240
+ (getVariableByName('valueBar/foreground', modes) as string | null) ?? '#000000'
241
+
242
+ const labelHeight = lineHeight
243
+ const barAreaHeight = Math.max(0, height - labelHeight - valueBarGap)
244
+
245
+ const total = bars.length
246
+ const computedMax =
247
+ max ?? bars.reduce((acc, bar) => Math.max(acc, bar.value), 0)
248
+ const safeMax = computedMax > 0 ? computedMax : 1
249
+
250
+ const resolvedBars = React.useMemo(
251
+ () =>
252
+ bars.map((bar, index) => {
253
+ const barModes = {
254
+ ...modes,
255
+ 'Emphasis / DataViz': defaultEmphasisFor(index, total),
256
+ ...(bar.modes || {}),
257
+ }
258
+ const ratio = Math.max(0, Math.min(1, bar.value / safeMax))
259
+ const tokenColor =
260
+ (getVariableByName('valueBar/bar/background', barModes) as string | null) ??
261
+ '#c9b7ff'
262
+ const bgColor = bar.color ?? tokenColor
263
+ return {
264
+ original: bar,
265
+ index,
266
+ barModes,
267
+ ratio,
268
+ bgColor,
269
+ }
270
+ }),
271
+ [bars, modes, safeMax, total]
272
+ )
273
+
274
+ const computedLabelStyle: TextStyle = {
275
+ color: foreground,
276
+ fontFamily,
277
+ fontSize,
278
+ lineHeight,
279
+ fontWeight,
280
+ textAlign: 'center',
281
+ }
282
+
283
+ const defaultAccessibilityLabel =
284
+ accessibilityLabel ??
285
+ `Comparison of ${total} bar${total === 1 ? '' : 's'}: ` +
286
+ bars
287
+ .map((bar, i) => {
288
+ const legend = typeof bar.legend === 'string' ? bar.legend : `bar ${i + 1}`
289
+ return `${legend} ${bar.value}`
290
+ })
291
+ .join(', ')
292
+
293
+ return (
294
+ <View
295
+ accessibilityLabel={defaultAccessibilityLabel}
296
+ style={[{ width: '100%' }, style]}
297
+ >
298
+ <View
299
+ accessibilityRole="image"
300
+ style={[
301
+ {
302
+ flexDirection: 'row',
303
+ alignItems: 'flex-end',
304
+ height,
305
+ gap: wrapGap,
306
+ width: '100%',
307
+ },
308
+ chartStyle,
309
+ ]}
310
+ >
311
+ {resolvedBars.map(({ original, index, ratio, bgColor }) => {
312
+ const barHeightPx = Math.max(0, barAreaHeight * ratio)
313
+ const valueBarTotalHeight = labelHeight + valueBarGap + barHeightPx
314
+ const hasLabel =
315
+ original.label !== undefined && original.label !== null && original.label !== false
316
+
317
+ return (
318
+ <View
319
+ key={original.key ?? `bar-${index}`}
320
+ accessibilityLabel={original.accessibilityLabel}
321
+ style={{
322
+ flex: 1,
323
+ flexDirection: 'column',
324
+ alignItems: 'stretch',
325
+ justifyContent: 'flex-end',
326
+ height: valueBarTotalHeight,
327
+ gap: valueBarGap,
328
+ minWidth: 1,
329
+ }}
330
+ >
331
+ {hasLabel ? (
332
+ <Text
333
+ numberOfLines={1}
334
+ style={[computedLabelStyle, labelStyle]}
335
+ >
336
+ {original.label}
337
+ </Text>
338
+ ) : null}
339
+
340
+ <BarShape height={barHeightPx} color={bgColor} radius={barRadius} />
341
+ </View>
342
+ )
343
+ })}
344
+ </View>
345
+
346
+ <View
347
+ style={[
348
+ {
349
+ marginTop: legendGap,
350
+ flexDirection: 'row',
351
+ alignItems: 'center',
352
+ justifyContent: 'space-between',
353
+ width: '100%',
354
+ },
355
+ legendStyle,
356
+ ]}
357
+ >
358
+ {resolvedBars.map(({ original, index, barModes, bgColor }) => {
359
+ const indicatorOverride = original.color ? bgColor : undefined
360
+ return (
361
+ <MetricLegendItem
362
+ key={original.key ?? `legend-${index}`}
363
+ label={original.legend}
364
+ value={original.legendValue}
365
+ modes={barModes}
366
+ {...(indicatorOverride !== undefined
367
+ ? { indicatorColor: indicatorOverride }
368
+ : {})}
369
+ style={{ flex: 1, minWidth: 0 }}
370
+ />
371
+ )
372
+ })}
373
+ </View>
374
+ </View>
375
+ )
376
+ }
377
+
378
+ export default CoverageBarComparison
@@ -0,0 +1,225 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ type StyleProp,
5
+ type TextStyle,
6
+ type ViewStyle,
7
+ } from 'react-native'
8
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
9
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
+ import Button, { type ButtonProps } from '../Button/Button'
12
+ import CircularProgressBar from '../CircularProgressBar/CircularProgressBar'
13
+
14
+ type CoverageRingBaseProps = Omit<
15
+ React.ComponentProps<typeof View>,
16
+ 'children' | 'style'
17
+ >
18
+
19
+ export type CoverageRingProps = CoverageRingBaseProps & {
20
+ /**
21
+ * Current count (numerator). Together with `total`, drives both the ring
22
+ * progress and the default `valueLabel` (`"{value} of {total}"`). Clamped
23
+ * to `[0, total]`.
24
+ */
25
+ value?: number
26
+ /**
27
+ * Maximum count (denominator). Drives the ring fill ratio along with
28
+ * `value`. Must be greater than 0; a non-positive total collapses to 0%.
29
+ */
30
+ total?: number
31
+ /**
32
+ * Small caption rendered above the value inside the ring (e.g. "Benefits
33
+ * availed"). Maps to the Figma `circularProgressBar/supportText/*` tokens.
34
+ */
35
+ supportText?: string
36
+ /**
37
+ * Optional override for the formatted value rendered inside the ring.
38
+ * Defaults to `"{value} of {total}"` so the visual matches the Figma
39
+ * "Coverage Ring" reference.
40
+ */
41
+ valueLabel?: string
42
+ /**
43
+ * Visible label of the default action button. Ignored when `action` is
44
+ * provided. Defaults to `"Learn more"` to match the Figma reference.
45
+ */
46
+ actionLabel?: string
47
+ /** Press handler for the default action button. */
48
+ onActionPress?: () => void
49
+ /**
50
+ * Forward extra props to the default action button (e.g. `disabled`,
51
+ * `accessibilityHint`, `icon`). Use this for tweaks; for a fully custom
52
+ * action node, pass it as `action` instead.
53
+ */
54
+ actionProps?: Partial<ButtonProps>
55
+ /**
56
+ * Render-prop slot for the action area. When provided, takes precedence
57
+ * over `actionLabel`/`onActionPress`/`actionProps` and the default button
58
+ * is skipped entirely.
59
+ *
60
+ * Use `children` instead if you prefer a JSX-style slot — both are
61
+ * supported and produce identical output.
62
+ */
63
+ action?: React.ReactNode
64
+ /**
65
+ * Alternative slot for the action area. Identical to `action`; provided
66
+ * for ergonomic JSX:
67
+ *
68
+ * ```tsx
69
+ * <CoverageRing value={4} total={7} supportText="Benefits availed">
70
+ * <CustomCTA />
71
+ * </CoverageRing>
72
+ * ```
73
+ */
74
+ children?: React.ReactNode
75
+ /** Design token modes forwarded to token lookups and slot children. */
76
+ modes?: Record<string, any>
77
+ /** Container style override. */
78
+ style?: StyleProp<ViewStyle>
79
+ /** Override the support-text style inside the ring. */
80
+ supportTextStyle?: StyleProp<TextStyle>
81
+ /** Override the value-label style inside the ring. */
82
+ valueStyle?: StyleProp<TextStyle>
83
+ /** Accessibility label for the entire component. */
84
+ accessibilityLabel?: string
85
+ }
86
+
87
+ const toNumber = (value: unknown, fallback: number) => {
88
+ if (typeof value === 'number') {
89
+ return Number.isFinite(value) ? value : fallback
90
+ }
91
+ if (typeof value === 'string') {
92
+ const parsed = Number(value)
93
+ return Number.isFinite(parsed) ? parsed : fallback
94
+ }
95
+ return fallback
96
+ }
97
+
98
+ const clamp = (value: number, min: number, max: number) =>
99
+ Math.min(max, Math.max(min, value))
100
+
101
+ // The Figma "Coverage Ring" pairs a `circularProgressBar Size: M` ring with a
102
+ // `Button / Size: S` Secondary button. Locking these in as component defaults
103
+ // keeps the visual identical to the Figma reference when the caller supplies
104
+ // only their own brand/theme modes — caller modes are still merged on top so
105
+ // any of these can be overridden per-instance.
106
+ const COMPONENT_DEFAULT_MODES: Readonly<Record<string, any>> = Object.freeze({
107
+ 'circularProgressBar Size': 'M',
108
+ 'Button / Size': 'S',
109
+ })
110
+
111
+ // Match the Figma source: regardless of the overall AppearanceBrand/Emphasis
112
+ // the caller picks (used to colour the ring), the Coverage Ring CTA renders
113
+ // with the Secondary brand at Medium emphasis — which resolves to the
114
+ // pale-lavender background (`#dbcfff`) + deep-purple foreground (`#5d00b5`)
115
+ // shown in Figma. Mirrors the pattern used by `CardFinancialCondition.tsx`.
116
+ const BUTTON_FORCED_BRAND: Readonly<Record<string, any>> = Object.freeze({
117
+ AppearanceBrand: 'Secondary',
118
+ Emphasis: 'Medium',
119
+ })
120
+
121
+ /**
122
+ * `CoverageRing` renders a single-purpose insight: how many items out of a
123
+ * total have been covered (e.g. "4 of 7 benefits availed"), paired with a
124
+ * call-to-action button below the ring.
125
+ *
126
+ * It composes the existing {@link CircularProgressBar} (in its `M` size so
127
+ * there is room for a caption + value inside the ring) and {@link Button}
128
+ * (in its `S`, Secondary brand variant) — both driven by the same design
129
+ * tokens used by the rest of the design system.
130
+ *
131
+ * The ring fill is derived from `value / total`, so the percentage, the
132
+ * displayed `"{value} of {total}"` label and the support caption stay in
133
+ * sync automatically. There is no separate `progress` prop.
134
+ *
135
+ * @component
136
+ */
137
+ function CoverageRing({
138
+ value = 4,
139
+ total = 7,
140
+ supportText = 'Benefits availed',
141
+ valueLabel,
142
+ actionLabel = 'Learn more',
143
+ onActionPress,
144
+ actionProps,
145
+ action,
146
+ children,
147
+ modes: propModes = EMPTY_MODES,
148
+ style,
149
+ supportTextStyle,
150
+ valueStyle,
151
+ accessibilityLabel,
152
+ ...rest
153
+ }: CoverageRingProps) {
154
+ const { modes: globalModes } = useTokens()
155
+
156
+ // Merge order matches the rest of the design system (see `Gauge.tsx`):
157
+ // 1. Component-level defaults — the lowest-priority safety net.
158
+ // 2. Global modes from `JFSThemeProvider` — app-wide theme.
159
+ // 3. Caller-supplied `propModes` — highest priority, overrides everything.
160
+ const modes = React.useMemo(
161
+ () => ({ ...COMPONENT_DEFAULT_MODES, ...globalModes, ...propModes }),
162
+ [propModes, globalModes]
163
+ )
164
+
165
+ // Force the Secondary brand on the button only; the ring stays on whatever
166
+ // brand the caller selected. Done via a forcedModes layer so callers can't
167
+ // accidentally bring back a Primary-coloured CTA through their own modes.
168
+ const buttonModes = React.useMemo(
169
+ () => ({ ...modes, ...BUTTON_FORCED_BRAND }),
170
+ [modes]
171
+ )
172
+
173
+ const safeTotal = total > 0 ? total : 0
174
+ const clampedValue = clamp(value, 0, safeTotal)
175
+ const progressPercent = safeTotal > 0 ? (clampedValue / safeTotal) * 100 : 0
176
+ const computedValueLabel = valueLabel ?? `${clampedValue} of ${safeTotal}`
177
+
178
+ const gap = toNumber(getVariableByName('coverageRing/gap', modes), 16)
179
+
180
+ const containerStyle: ViewStyle = {
181
+ alignItems: 'center',
182
+ gap,
183
+ }
184
+
185
+ // Custom action slot resolution order: explicit `action` prop wins over
186
+ // `children`. We pass `modes` down via cloneChildrenWithModes so any
187
+ // token-driven child (e.g. a custom `<Button>`) inherits the parent theme.
188
+ const customAction = action ?? children
189
+ const hasCustomAction = customAction !== undefined && customAction !== null
190
+
191
+ const defaultAccessibilityLabel =
192
+ accessibilityLabel ?? `${supportText}, ${computedValueLabel}`
193
+
194
+ return (
195
+ <View
196
+ accessibilityLabel={defaultAccessibilityLabel}
197
+ style={[containerStyle, style]}
198
+ {...rest}
199
+ >
200
+ <CircularProgressBar
201
+ state="Active"
202
+ value={progressPercent}
203
+ valueLabel={computedValueLabel}
204
+ supportText={supportText}
205
+ supportTextStyle={supportTextStyle}
206
+ valueStyle={valueStyle}
207
+ modes={modes}
208
+ accessibilityLabel={`${supportText}, ${computedValueLabel}`}
209
+ />
210
+
211
+ {hasCustomAction ? (
212
+ cloneChildrenWithModes(customAction, buttonModes)
213
+ ) : (
214
+ <Button
215
+ label={actionLabel}
216
+ modes={buttonModes}
217
+ {...(onActionPress !== undefined ? { onPress: onActionPress } : {})}
218
+ {...actionProps}
219
+ />
220
+ )}
221
+ </View>
222
+ )
223
+ }
224
+
225
+ export default CoverageRing