jfs-components 0.0.72 → 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 (116) hide show
  1. package/CHANGELOG.md +11 -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/CardBankAccount/CardBankAccount.js +213 -0
  5. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  6. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  7. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +125 -0
  8. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  9. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  10. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  11. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  12. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  13. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  14. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  15. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  16. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  17. package/lib/commonjs/components/OTP/OTP.js +381 -37
  18. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  19. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  20. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  21. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  22. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  23. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  24. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  25. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  26. package/lib/commonjs/components/index.js +171 -1
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/registry.js +1 -1
  29. package/lib/commonjs/utils/index.js +7 -0
  30. package/lib/commonjs/utils/number-utils.js +57 -0
  31. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  32. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  33. package/lib/module/components/CardBankAccount/CardBankAccount.js +208 -0
  34. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  35. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  36. package/lib/module/components/CheckboxItem/CheckboxItem.js +119 -0
  37. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  38. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  39. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  40. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  41. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  42. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  43. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  44. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  45. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  46. package/lib/module/components/OTP/OTP.js +381 -38
  47. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  48. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  49. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  50. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  51. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  52. package/lib/module/components/StatItem/StatItem.js +66 -36
  53. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  54. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  55. package/lib/module/components/index.js +21 -1
  56. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  57. package/lib/module/icons/registry.js +1 -1
  58. package/lib/module/utils/index.js +2 -1
  59. package/lib/module/utils/number-utils.js +53 -0
  60. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  61. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  62. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +79 -0
  63. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  64. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  65. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +56 -0
  66. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  67. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  68. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  69. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  70. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  71. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  72. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  73. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  74. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  75. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  76. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  77. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  78. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  79. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  80. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  81. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  82. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  83. package/lib/typescript/src/components/index.d.ts +22 -2
  84. package/lib/typescript/src/icons/registry.d.ts +1 -1
  85. package/lib/typescript/src/utils/index.d.ts +1 -0
  86. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  87. package/package.json +1 -1
  88. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  89. package/src/components/BrandChip/BrandChip.tsx +235 -0
  90. package/src/components/CardBankAccount/CardBankAccount.tsx +295 -0
  91. package/src/components/CardInsight/CardInsight.tsx +239 -0
  92. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  93. package/src/components/CheckboxItem/CheckboxItem.tsx +174 -0
  94. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  95. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  96. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  97. package/src/components/DonutChart/DonutChart.tsx +503 -0
  98. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  99. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  100. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  101. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  102. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  103. package/src/components/OTP/OTP.tsx +476 -29
  104. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  105. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  106. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  107. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  108. package/src/components/StatGroup/StatGroup.tsx +169 -0
  109. package/src/components/StatItem/StatItem.tsx +117 -40
  110. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  111. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  112. package/src/components/index.ts +32 -2
  113. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  114. package/src/icons/registry.ts +1 -1
  115. package/src/utils/index.ts +1 -0
  116. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem';
9
+ import SegmentedTrack from '../SegmentedTrack/SegmentedTrack';
10
+ import Tabs from '../Tabs/Tabs';
11
+ import TabItem from '../Tabs/TabItem';
12
+
13
+ /**
14
+ * One row of data inside a `RangeTrack` tab.
15
+ *
16
+ * Each item drives BOTH a `SegmentedTrack` segment and the matching
17
+ * `MetricLegendItem` row, so the segment color and legend indicator
18
+ * always share the same color by construction (same `Emphasis / DataViz`
19
+ * cascade as the standalone `SegmentedTrack`).
20
+ */
21
+
22
+ /**
23
+ * One tab inside a `RangeTrack`. Each tab carries its own `items`
24
+ * array, which is the single source of truth for both the
25
+ * `SegmentedTrack` segments AND the `MetricLegendItem` legend rows
26
+ * rendered for that tab.
27
+ */
28
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
29
+ const DEFAULT_EMPHASIS_CYCLE = ['High', 'Medium', 'Low'];
30
+ const DEFAULT_TABS = [{
31
+ label: 'Tab item',
32
+ items: [{
33
+ label: 'Large cap',
34
+ value: 1,
35
+ displayValue: '25%'
36
+ }, {
37
+ label: 'Mid cap',
38
+ value: 1,
39
+ displayValue: '25%'
40
+ }, {
41
+ label: 'Small cap',
42
+ value: 1,
43
+ displayValue: '25%'
44
+ }]
45
+ }, {
46
+ label: 'Tab item',
47
+ items: [{
48
+ label: 'Large cap',
49
+ value: 1,
50
+ displayValue: '25%'
51
+ }, {
52
+ label: 'Mid cap',
53
+ value: 1,
54
+ displayValue: '25%'
55
+ }, {
56
+ label: 'Small cap',
57
+ value: 1,
58
+ displayValue: '25%'
59
+ }]
60
+ }, {
61
+ label: 'Tab item',
62
+ items: [{
63
+ label: 'Large cap',
64
+ value: 1,
65
+ displayValue: '25%'
66
+ }, {
67
+ label: 'Mid cap',
68
+ value: 1,
69
+ displayValue: '25%'
70
+ }, {
71
+ label: 'Small cap',
72
+ value: 1,
73
+ displayValue: '25%'
74
+ }]
75
+ }];
76
+ const defaultEmphasisFor = index => DEFAULT_EMPHASIS_CYCLE[index % DEFAULT_EMPHASIS_CYCLE.length];
77
+
78
+ /**
79
+ * Resolve the shared color for an item. Honors any explicit `color`
80
+ * override, then falls back to `dataViz/bg` for the merged mode set,
81
+ * then to the Figma reference value.
82
+ *
83
+ * Mirrors the cascade used by `SegmentedTrack` (per-index `Emphasis /
84
+ * DataViz` defaults of `High` → `Medium` → `Low`, cycling) so the
85
+ * pre-computed color we pass to both the segment and the legend
86
+ * indicator matches what the `SegmentedTrack` would have computed on
87
+ * its own. This is what keeps segments and legend rows in sync by
88
+ * construction.
89
+ */
90
+ function resolveItemColor(parentModes, item, index) {
91
+ if (item.color) return item.color;
92
+ const itemModes = {
93
+ ...parentModes,
94
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
95
+ ...(item.modes || {})
96
+ };
97
+ return getVariableByName('dataViz/bg', itemModes) ?? '#cea15a';
98
+ }
99
+
100
+ /**
101
+ * Resolve what to render in the legend row's right-side slot. The
102
+ * order of precedence is:
103
+ * 1. `item.displayValue` if explicitly provided (including `null` /
104
+ * `false`, which the underlying `MetricLegendItem` treats as
105
+ * "hide the value slot").
106
+ * 2. `formatValue(item.value)` when a parent-level formatter exists.
107
+ * 3. `undefined` — the value slot is hidden.
108
+ */
109
+ function resolveLegendValue(item, formatValue) {
110
+ if (item.displayValue !== undefined) return item.displayValue;
111
+ if (formatValue) return formatValue(item.value);
112
+ return undefined;
113
+ }
114
+
115
+ /**
116
+ * `RangeTrack` pairs a tab row with a `SegmentedTrack` and a vertical
117
+ * stack of `MetricLegendItem` rows. Each tab carries its own `items`
118
+ * array which is the **single source of truth** for both the segments
119
+ * and the legend rows of that tab — every segment has exactly one
120
+ * legend row and they share the same color through the same
121
+ * `Emphasis / DataViz` cascade as the standalone `SegmentedTrack`.
122
+ *
123
+ * Switching tabs swaps the segments and the legend together so the
124
+ * two visualizations can never drift out of sync.
125
+ *
126
+ * The default 3-item layout per tab receives per-index
127
+ * `Emphasis / DataViz` defaults (item 1 → `High`, 2 → `Medium`, 3 →
128
+ * `Low`, then cycles). Override `Appearance / DataViz` on the parent
129
+ * `modes` to retheme the whole component, or set `modes` per item to
130
+ * remix.
131
+ *
132
+ * The component supports both **controlled** and **uncontrolled**
133
+ * tab selection — pass `activeTabIndex` + `onTabChange` for the
134
+ * controlled mode, or omit them and the component will manage the
135
+ * selection internally starting from `defaultActiveTabIndex`.
136
+ *
137
+ * @component
138
+ * @param {RangeTrackProps} props
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * <RangeTrack
143
+ * tabs={[
144
+ * {
145
+ * label: 'Sectoral',
146
+ * items: [
147
+ * { label: 'Large cap', value: 50, displayValue: '50%' },
148
+ * { label: 'Mid cap', value: 30, displayValue: '30%' },
149
+ * { label: 'Small cap', value: 20, displayValue: '20%' },
150
+ * ],
151
+ * },
152
+ * {
153
+ * label: 'Geography',
154
+ * items: [
155
+ * { label: 'India', value: 70, displayValue: '70%' },
156
+ * { label: 'US', value: 20, displayValue: '20%' },
157
+ * { label: 'Other', value: 10, displayValue: '10%' },
158
+ * ],
159
+ * },
160
+ * ]}
161
+ * />
162
+ * ```
163
+ */
164
+ function RangeTrack({
165
+ tabs,
166
+ formatValue,
167
+ activeTabIndex,
168
+ defaultActiveTabIndex = 0,
169
+ onTabChange,
170
+ scrollableTabs = false,
171
+ modes: propModes = EMPTY_MODES,
172
+ style,
173
+ tabsStyle,
174
+ trackStyle,
175
+ legendStyle,
176
+ accessibilityLabel
177
+ }) {
178
+ const {
179
+ modes: globalModes
180
+ } = useTokens();
181
+ const modes = {
182
+ ...globalModes,
183
+ ...propModes
184
+ };
185
+ const resolvedTabs = tabs && tabs.length > 0 ? tabs : DEFAULT_TABS;
186
+ const [internalIndex, setInternalIndex] = useState(() => clampIndex(defaultActiveTabIndex, resolvedTabs.length));
187
+ const isControlled = activeTabIndex !== undefined;
188
+ const rawIndex = isControlled ? activeTabIndex : internalIndex;
189
+ const safeIndex = clampIndex(rawIndex, resolvedTabs.length);
190
+ const handleTabPress = useCallback(index => {
191
+ const nextTab = resolvedTabs[index];
192
+ if (!nextTab) return;
193
+ if (!isControlled) setInternalIndex(index);
194
+ onTabChange?.(index, nextTab);
195
+ }, [isControlled, onTabChange, resolvedTabs]);
196
+ const containerGap = getVariableByName('rangeTrack/gap', modes) ?? 28;
197
+ // Vertical gap between legend rows is not exposed as its own token —
198
+ // the Figma design uses 8px between rows, mirroring the
199
+ // `donutChartSummary/legend/gap` default. Keep the value in step
200
+ // with `DonutChartSummary` so the two summary components feel
201
+ // cohesive when stacked.
202
+ const legendRowGap = 8;
203
+ const activeTab = resolvedTabs[safeIndex];
204
+ const activeItems = activeTab?.items ?? [];
205
+ const segments = activeItems.map((item, index) => ({
206
+ key: item.key ?? `segment-${index}`,
207
+ value: item.value,
208
+ color: resolveItemColor(modes, item, index),
209
+ accessibilityLabel: item.accessibilityLabel
210
+ }));
211
+ return /*#__PURE__*/_jsxs(View, {
212
+ accessibilityRole: "summary",
213
+ accessibilityLabel: accessibilityLabel,
214
+ style: [{
215
+ width: '100%',
216
+ flexDirection: 'column',
217
+ alignItems: 'flex-start',
218
+ gap: containerGap
219
+ }, style],
220
+ children: [/*#__PURE__*/_jsx(Tabs, {
221
+ modes: modes,
222
+ scrollable: scrollableTabs,
223
+ style: tabsStyle,
224
+ children: resolvedTabs.map((tab, index) => /*#__PURE__*/_jsx(TabItem, {
225
+ label: tab.label,
226
+ active: index === safeIndex,
227
+ onPress: () => handleTabPress(index),
228
+ accessibilityLabel: tab.accessibilityLabel ?? tab.label
229
+ }, tab.key ?? tab.label ?? `tab-${index}`))
230
+ }), /*#__PURE__*/_jsx(SegmentedTrack, {
231
+ modes: modes,
232
+ segments: segments,
233
+ style: trackStyle,
234
+ accessibilityLabel: activeTab?.accessibilityLabel ?? activeTab?.label
235
+ }), /*#__PURE__*/_jsx(View, {
236
+ style: [{
237
+ width: '100%',
238
+ flexDirection: 'column',
239
+ alignItems: 'flex-start',
240
+ gap: legendRowGap
241
+ }, legendStyle],
242
+ children: activeItems.map((item, index) => /*#__PURE__*/_jsx(MetricLegendItem, {
243
+ label: item.label,
244
+ value: resolveLegendValue(item, formatValue),
245
+ indicatorColor: resolveItemColor(modes, item, index),
246
+ modes: modes
247
+ }, item.key ?? `legend-${index}`))
248
+ })]
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Clamp a tab index into `[0, length)`. Negative or out-of-bounds
254
+ * values fall back to `0` to avoid rendering an undefined tab.
255
+ */
256
+ function clampIndex(index, length) {
257
+ if (length <= 0) return 0;
258
+ if (!Number.isFinite(index)) return 0;
259
+ if (index < 0) return 0;
260
+ if (index >= length) return length - 1;
261
+ return Math.floor(index);
262
+ }
263
+ export default RangeTrack;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+ import { formatIndianNumber } from '../../utils/number-utils';
8
+ import Title from '../Title/Title';
9
+ import LinearProgress from '../LinearProgress/LinearProgress';
10
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem';
11
+
12
+ /**
13
+ * A single row in the savings-goal legend (current vs. target).
14
+ *
15
+ * `value` is a **numeric amount** (e.g. `240000`). It serves two purposes:
16
+ *
17
+ * 1. Display: rendered on the right side of the row using Indian numeric
18
+ * notation via {@link formatIndianNumber} and prefixed with `currency`
19
+ * (e.g. `240000` → `"₹2.4L"`).
20
+ * 2. Progress derivation: the bar fills to `current.value / target.value`
21
+ * automatically. There is no separate `progress` prop.
22
+ *
23
+ * Pass `value: undefined` to render a label-only row (the underlying
24
+ * {@link MetricLegendItem} hides the right slot in that case).
25
+ */
26
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
27
+ const DEFAULT_LEGEND_PADDING = 8;
28
+ const DEFAULT_MODES = Object.freeze({
29
+ 'LinearProgress Size': 'L'
30
+ });
31
+ const DEFAULT_CURRENT = {
32
+ label: 'Current (4 months)',
33
+ value: 240000
34
+ };
35
+ const DEFAULT_TARGET = {
36
+ label: 'Recommended (8 months)',
37
+ value: 480000
38
+ };
39
+
40
+ /**
41
+ * `SavingsGoalSummary` visualises progress toward a savings goal as:
42
+ *
43
+ * 1. A `Title` showing the percentage — **computed automatically** from
44
+ * `current.value / target.value`. There is no `progress` prop; the
45
+ * component owns the calculation so the title, bar and legend are always
46
+ * in sync.
47
+ * 2. A `LinearProgress` bar driven by the same derived ratio.
48
+ * 3. A two-row legend comparing **current** vs. **target**, where each numeric
49
+ * `value` is auto-formatted with Indian notation
50
+ * ({@link formatIndianNumber}) and prefixed with the `currency` symbol.
51
+ *
52
+ * The component is intentionally narrow in scope — it is the body of a savings
53
+ * insight card. Wrap it in `CardInsight` (or any container of your choice) to
54
+ * add a heading, badge or footer.
55
+ *
56
+ * @component
57
+ * @param {SavingsGoalSummaryProps} props
58
+ */
59
+ function SavingsGoalSummary({
60
+ currency = '₹',
61
+ current = DEFAULT_CURRENT,
62
+ target = DEFAULT_TARGET,
63
+ children,
64
+ modes = EMPTY_MODES,
65
+ style,
66
+ titleStyle,
67
+ legendStyle
68
+ }) {
69
+ // Merge caller modes on top of the defaults so callers can override
70
+ // (e.g. switch to `LinearProgress Size: M`) while still receiving the
71
+ // sensible component-level default.
72
+ const mergedModes = useMemo(() => modes === EMPTY_MODES ? DEFAULT_MODES : {
73
+ ...DEFAULT_MODES,
74
+ ...modes
75
+ }, [modes]);
76
+
77
+ // Resolve the `LinearProgress` track / indicator colours so the legend
78
+ // dots automatically stay in sync with the progress bar without callers
79
+ // needing to plumb through `indicatorColor`.
80
+ //
81
+ // The token names AND the merge strategy must match `LinearProgress.tsx`
82
+ // exactly, otherwise the dot colours can drift from the bar:
83
+ // • Token aliases live in the `Emphasis` collection (modes High|Medium|Low),
84
+ // NOT `Emphasis / DataViz` — passing the wrong mode key would collapse
85
+ // both dots to the same default-mode colour.
86
+ // • Defaults are placed *before* `mergedModes` is spread, so callers can
87
+ // still override `Emphasis` via the `modes` prop (matches
88
+ // `LinearProgress`'s philosophy).
89
+ const indicatorColorFromTokens = getVariableByName('linearProgress/indicator/background', {
90
+ Emphasis: 'High',
91
+ ...mergedModes
92
+ }) ?? '#5d00b5';
93
+ const trackColorFromTokens = getVariableByName('linearProgress/track/background', {
94
+ Emphasis: 'Low',
95
+ ...mergedModes
96
+ }) ?? '#ede7ff';
97
+
98
+ // Single source of truth for the bar fill, the title percentage and the
99
+ // formatted legend amounts. There is intentionally no consumer-facing
100
+ // `progress` prop — the only way to change the bar is to change the
101
+ // numeric `current` / `target` values. This keeps the three views (title,
102
+ // bar, legend) impossible to desynchronise.
103
+ const resolvedProgress = useMemo(() => {
104
+ const cv = current?.value;
105
+ const tv = target?.value;
106
+ if (typeof cv !== 'number' || typeof tv !== 'number' || tv <= 0) {
107
+ return 0;
108
+ }
109
+ return Math.min(Math.max(cv / tv, 0), 1);
110
+ }, [current, target]);
111
+ const percentageLabel = `${Math.round(resolvedProgress * 100)}%`;
112
+ const gap = getVariableByName('savingsGoalSummary/gap', mergedModes) ?? 23;
113
+ const legendGap = getVariableByName('savingsGoalSummary/legend/gap', mergedModes) ?? 16;
114
+ const customLegend = children ? cloneChildrenWithModes(children, mergedModes) : null;
115
+ const defaultLegend = !customLegend && (current || target) ? /*#__PURE__*/_jsxs(_Fragment, {
116
+ children: [current && /*#__PURE__*/_jsx(MetricLegendItem, {
117
+ modes: mergedModes,
118
+ label: current.label,
119
+ value: formatLegendValue(current.value, currency),
120
+ indicatorColor: current.indicatorColor ?? indicatorColorFromTokens
121
+ }), target && /*#__PURE__*/_jsx(MetricLegendItem, {
122
+ modes: mergedModes,
123
+ label: target.label,
124
+ value: formatLegendValue(target.value, currency),
125
+ indicatorColor: target.indicatorColor ?? trackColorFromTokens
126
+ })]
127
+ }) : null;
128
+ const legendNode = customLegend ?? defaultLegend;
129
+ const showLegend = legendNode != null;
130
+ return /*#__PURE__*/_jsxs(View, {
131
+ style: [{
132
+ width: '100%',
133
+ gap,
134
+ alignItems: 'stretch'
135
+ }, style],
136
+ accessibilityLabel: `Savings goal progress, ${percentageLabel}`,
137
+ children: [/*#__PURE__*/_jsx(Title, {
138
+ title: percentageLabel,
139
+ modes: mergedModes,
140
+ style: TITLE_CONTAINER_STYLE,
141
+ textStyle: titleStyle
142
+ }), /*#__PURE__*/_jsx(LinearProgress, {
143
+ value: resolvedProgress,
144
+ modes: mergedModes
145
+ }), showLegend && /*#__PURE__*/_jsx(View, {
146
+ style: [{
147
+ width: '100%',
148
+ padding: DEFAULT_LEGEND_PADDING,
149
+ gap: legendGap,
150
+ alignItems: 'stretch'
151
+ }, legendStyle],
152
+ children: legendNode
153
+ })]
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Format a single legend `value` for display. Returns `undefined` when the
159
+ * value is missing so the underlying {@link MetricLegendItem} hides the right
160
+ * slot (matches the Figma `data` toggle = off).
161
+ */
162
+ function formatLegendValue(value, currency) {
163
+ if (typeof value !== 'number') return undefined;
164
+ return formatIndianNumber(value, {
165
+ prefix: currency
166
+ });
167
+ }
168
+
169
+ // Neutralise the `Title` component's default page-level padding so it sits
170
+ // flush inside the summary card (the parent container owns spacing via `gap`).
171
+ const TITLE_CONTAINER_STYLE = {
172
+ paddingHorizontal: 0,
173
+ paddingVertical: 0
174
+ };
175
+ export default SavingsGoalSummary;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils';
8
+
9
+ /**
10
+ * Per-segment data definition for the data-driven `segments` prop.
11
+ *
12
+ * Use `value` for proportional widths (segments share width by their value
13
+ * relative to the sum). When `value` is omitted, segments share the row
14
+ * equally (`flex: 1`).
15
+ *
16
+ * Use `modes` to override the per-segment design-token mode — typically the
17
+ * `Appearance / DataViz` mode for color theming, or `Emphasis / DataViz` to
18
+ * change the emphasis level. The parent already injects per-index defaults
19
+ * (segment 0 = High, 1 = Medium, 2 = Low, then cycling) so callers only need
20
+ * to pass `modes` when they want a different result.
21
+ */
22
+ import { jsx as _jsx } from "react/jsx-runtime";
23
+ /**
24
+ * Default per-index Emphasis modes applied to every segment when the caller
25
+ * does not provide its own `Emphasis / DataViz` override. Cycles for >3
26
+ * segments so additional segments fall back to the same High/Medium/Low
27
+ * rotation.
28
+ */
29
+ const DEFAULT_EMPHASIS_CYCLE = ['High', 'Medium', 'Low'];
30
+ const DEFAULT_SEGMENTS = [{}, {}, {}];
31
+
32
+ /**
33
+ * Compute the default `Emphasis / DataViz` mode for a segment at `index`.
34
+ * Cycles through {@link DEFAULT_EMPHASIS_CYCLE} so any number of segments
35
+ * gets a sensible default.
36
+ */
37
+ function defaultEmphasisFor(index) {
38
+ return DEFAULT_EMPHASIS_CYCLE[index % DEFAULT_EMPHASIS_CYCLE.length];
39
+ }
40
+ function SegmentedTrackSegment({
41
+ value = 1,
42
+ color,
43
+ modes = EMPTY_MODES,
44
+ style,
45
+ accessibilityLabel
46
+ }) {
47
+ const resolvedColor = color ?? getVariableByName('dataViz/bg', modes) ?? '#5d00b5';
48
+ return /*#__PURE__*/_jsx(View, {
49
+ accessibilityLabel: accessibilityLabel,
50
+ style: [{
51
+ flex: value,
52
+ minWidth: 1,
53
+ height: '100%',
54
+ backgroundColor: resolvedColor
55
+ }, style]
56
+ });
57
+ }
58
+
59
+ /**
60
+ * `SegmentedTrack` renders a horizontal pill-shaped row of categorical
61
+ * segments. Use it for distribution / share-of breakdowns where each
62
+ * segment is a sibling category (not a directional or temporal progress).
63
+ *
64
+ * Defaults to three equal segments tinted at descending Emphasis levels
65
+ * (`High`, `Medium`, `Low`) so the shape reads as a single concept split
66
+ * three ways. Consumers can either pass the data-driven `segments` prop or
67
+ * a fully custom `children` slot of `SegmentedTrack.Segment`s.
68
+ *
69
+ * Each segment resolves its color through the `dataViz/bg` token, which
70
+ * cascades through `Emphasis / DataViz` and `Appearance / DataViz`. Pass
71
+ * `modes` per segment (or override `Appearance / DataViz` at the parent
72
+ * level via `modes`) to retheme the row without touching colors directly.
73
+ *
74
+ * @component
75
+ * @param {SegmentedTrackProps} props
76
+ */
77
+ function SegmentedTrack({
78
+ segments,
79
+ children,
80
+ modes: propModes = EMPTY_MODES,
81
+ style,
82
+ segmentStyle,
83
+ accessibilityLabel
84
+ }) {
85
+ const {
86
+ modes: globalModes
87
+ } = useTokens();
88
+ const modes = {
89
+ ...globalModes,
90
+ ...propModes
91
+ };
92
+ const trackHeight = getVariableByName('segmentedTrack/height', modes) ?? 24;
93
+ const trackRadius = getVariableByName('segmentedTrack/radius', modes) ?? 999;
94
+ const renderedSegments = renderSegments({
95
+ segments,
96
+ children,
97
+ modes,
98
+ segmentStyle
99
+ });
100
+ return /*#__PURE__*/_jsx(View, {
101
+ accessibilityRole: "image",
102
+ accessibilityLabel: accessibilityLabel,
103
+ style: [{
104
+ flexDirection: 'row',
105
+ alignItems: 'stretch',
106
+ height: trackHeight,
107
+ borderRadius: trackRadius,
108
+ overflow: 'hidden',
109
+ width: '100%',
110
+ backgroundColor: 'transparent'
111
+ }, style],
112
+ children: renderedSegments
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Build the slot children. When the caller passes JSX `children`, every
118
+ * top-level element is treated as one segment and receives merged `modes`
119
+ * (parent + per-index Emphasis default + the child's own `modes` taking
120
+ * priority). Otherwise the data-driven `segments` array is rendered.
121
+ */
122
+ function renderSegments({
123
+ segments,
124
+ children,
125
+ modes,
126
+ segmentStyle
127
+ }) {
128
+ if (children !== undefined && children !== null) {
129
+ const flat = flattenChildren(children);
130
+ return flat.map((child, index) => {
131
+ if (! /*#__PURE__*/React.isValidElement(child)) {
132
+ return child;
133
+ }
134
+ const childProps = child.props ?? {};
135
+ const childModes = childProps.modes;
136
+ const mergedModes = {
137
+ ...modes,
138
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
139
+ ...(childModes || {})
140
+ };
141
+ return /*#__PURE__*/React.cloneElement(child, {
142
+ ...childProps,
143
+ modes: mergedModes,
144
+ key: child.key ?? `segment-${index}`
145
+ });
146
+ });
147
+ }
148
+ const list = segments && segments.length > 0 ? segments : DEFAULT_SEGMENTS;
149
+ return list.map((segment, index) => {
150
+ const segmentModes = {
151
+ ...modes,
152
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
153
+ ...(segment.modes || {})
154
+ };
155
+ return /*#__PURE__*/_jsx(SegmentedTrackSegment, {
156
+ value: segment.value ?? 1,
157
+ color: segment.color,
158
+ modes: segmentModes,
159
+ style: [segmentStyle, segment.style],
160
+ accessibilityLabel: segment.accessibilityLabel
161
+ }, segment.key ?? `segment-${index}`);
162
+ });
163
+ }
164
+ SegmentedTrack.Segment = SegmentedTrackSegment;
165
+ export { SegmentedTrackSegment };
166
+ export default SegmentedTrack;