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,236 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ type ImageSourcePropType,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
12
+ import Image from '../Image/Image'
13
+ import ProductLabel from '../ProductLabel/ProductLabel'
14
+
15
+ export type ProductOverviewStat = {
16
+ /** The large prominent value shown on top, e.g. "995", "3%". */
17
+ value: string
18
+ /** The descriptive label rendered beneath the value, e.g. "Purity". */
19
+ label: string
20
+ }
21
+
22
+ export type ProductOverviewProps = {
23
+ /** Hero image source rendered at the top of the card. */
24
+ imageSource?: ImageSourcePropType | string
25
+ /** Aspect ratio for the hero image. Defaults to the Figma frame (288:170). */
26
+ imageRatio?: number
27
+ /** Avatar source for the inline ProductLabel chip. */
28
+ labelImageSource?: ImageSourcePropType | string
29
+ /** Text shown next to the avatar in the ProductLabel chip. */
30
+ label?: string
31
+ /** The large product name, e.g. "0.5g Gold Coin". */
32
+ productName?: string
33
+ /** The short multi-line description below the product name. */
34
+ description?: string
35
+ /**
36
+ * Stats rendered in the bottom row. Each stat shows a large value above a
37
+ * smaller label. Pass an empty array (or `null`) to hide the row.
38
+ */
39
+ stats?: ProductOverviewStat[]
40
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
41
+ modes?: Record<string, any>
42
+ /** Container style override. */
43
+ style?: StyleProp<ViewStyle>
44
+ /**
45
+ * Custom slot rendered between the description and the stats row, useful
46
+ * for badges, callouts, etc. Receives the same `modes` as the parent.
47
+ */
48
+ children?: React.ReactNode
49
+ }
50
+
51
+ const DEFAULT_STATS: ProductOverviewStat[] = [
52
+ { value: '995', label: 'Purity' },
53
+ { value: '3%', label: 'GST' },
54
+ ]
55
+
56
+ const ProductOverview = ({
57
+ imageSource,
58
+ imageRatio = 288 / 170,
59
+ labelImageSource,
60
+ label = 'Gold',
61
+ productName = '0.5g Gold Coin',
62
+ description = 'Your gold is insured from our vault to you. If lost or damaged, we’ll replace it.',
63
+ stats = DEFAULT_STATS,
64
+ modes = EMPTY_MODES,
65
+ style,
66
+ children,
67
+ }: ProductOverviewProps) => {
68
+ const padding = (getVariableByName('productOverview/padding', modes) as number | null) ?? 24
69
+ const gap = (getVariableByName('productOverview/gap', modes) as number | null) ?? 12
70
+ const background =
71
+ (getVariableByName('productOverview/background', modes) as string | null) ?? '#ffffff'
72
+
73
+ const productNameColor =
74
+ (getVariableByName('productOverview/productName/color', modes) as string | null) ??
75
+ '#0d0d0f'
76
+ const productNameFontFamily =
77
+ (getVariableByName('productOverview/productName/fontFamily', modes) as string | null) ??
78
+ 'JioType Var'
79
+ const productNameFontSize =
80
+ (getVariableByName('productOverview/productName/fontSize', modes) as number | null) ?? 26
81
+ const productNameFontWeight =
82
+ getVariableByName('productOverview/productName/fontWeight', modes) ?? 900
83
+ const productNameLineHeight =
84
+ (getVariableByName('productOverview/productName/lineHeight', modes) as number | null) ?? 26
85
+
86
+ const descriptionColor =
87
+ (getVariableByName('productOverview/description/color', modes) as string | null) ??
88
+ '#1a1c1f'
89
+ const descriptionFontFamily =
90
+ (getVariableByName('productOverview/description/fontFamily', modes) as string | null) ??
91
+ 'JioType Var'
92
+ const descriptionFontSize =
93
+ (getVariableByName('productOverview/description/fontSize', modes) as number | null) ?? 14
94
+ const descriptionFontWeight =
95
+ getVariableByName('productOverview/description/fontWeight', modes) ?? 500
96
+ const descriptionLineHeight =
97
+ (getVariableByName('productOverview/description/lineHeight', modes) as number | null) ??
98
+ 18.2
99
+
100
+ const statGap = (getVariableByName('productOverview/stat/gap', modes) as number | null) ?? 2
101
+ const statValueColor =
102
+ (getVariableByName('productOverview/stat/value/color', modes) as string | null) ??
103
+ '#141414'
104
+ const statValueFontFamily =
105
+ (getVariableByName('productOverview/stat/value/fontFamily', modes) as string | null) ??
106
+ 'JioType Var'
107
+ const statValueFontSize =
108
+ (getVariableByName('productOverview/stat/value/fontSize', modes) as number | null) ?? 20
109
+ const statValueFontWeight =
110
+ getVariableByName('productOverview/stat/value/fontWeight', modes) ?? 900
111
+ const statValueLineHeight =
112
+ (getVariableByName('productOverview/stat/value/lineHeight', modes) as number | null) ?? 20
113
+
114
+ const statLabelColor = productNameColor
115
+ const statLabelFontFamily =
116
+ (getVariableByName('productOverview/stat/label/fontFamily', modes) as string | null) ??
117
+ 'JioType Var'
118
+ const statLabelFontSize =
119
+ (getVariableByName('productOverview/stat/label/fontSize', modes) as number | null) ?? 12
120
+ const statLabelFontWeight =
121
+ getVariableByName('productOverview/stat/label/fontWeight', modes) ?? 400
122
+ const statLabelLineHeight =
123
+ (getVariableByName('productOverview/stat/label/lineHeight', modes) as number | null) ??
124
+ 15.6
125
+
126
+ const productNameStyle: TextStyle = {
127
+ color: productNameColor,
128
+ fontFamily: productNameFontFamily,
129
+ fontSize: productNameFontSize,
130
+ fontWeight: String(productNameFontWeight) as TextStyle['fontWeight'],
131
+ lineHeight: productNameLineHeight,
132
+ textAlign: 'center',
133
+ }
134
+
135
+ const descriptionStyle: TextStyle = {
136
+ color: descriptionColor,
137
+ fontFamily: descriptionFontFamily,
138
+ fontSize: descriptionFontSize,
139
+ fontWeight: String(descriptionFontWeight) as TextStyle['fontWeight'],
140
+ lineHeight: descriptionLineHeight,
141
+ textAlign: 'center',
142
+ }
143
+
144
+ const statValueStyle: TextStyle = {
145
+ color: statValueColor,
146
+ fontFamily: statValueFontFamily,
147
+ fontSize: statValueFontSize,
148
+ fontWeight: String(statValueFontWeight) as TextStyle['fontWeight'],
149
+ lineHeight: statValueLineHeight,
150
+ }
151
+
152
+ const statLabelStyle: TextStyle = {
153
+ color: statLabelColor,
154
+ fontFamily: statLabelFontFamily,
155
+ fontSize: statLabelFontSize,
156
+ fontWeight: String(statLabelFontWeight) as TextStyle['fontWeight'],
157
+ lineHeight: statLabelLineHeight,
158
+ textAlign: 'center',
159
+ }
160
+
161
+ const showStats = Array.isArray(stats) && stats.length > 0
162
+
163
+ return (
164
+ <View
165
+ style={[
166
+ {
167
+ backgroundColor: background,
168
+ padding,
169
+ gap,
170
+ alignItems: 'center',
171
+ width: '100%',
172
+ },
173
+ style,
174
+ ]}
175
+ >
176
+ {imageSource != null && (
177
+ <Image
178
+ imageSource={imageSource}
179
+ ratio={imageRatio}
180
+ resizeMode="contain"
181
+ accessibilityElementsHidden
182
+ importantForAccessibility="no"
183
+ />
184
+ )}
185
+
186
+ <ProductLabel
187
+ label={label}
188
+ {...(labelImageSource != null && { imageSource: labelImageSource })}
189
+ modes={modes}
190
+ />
191
+
192
+ {productName ? (
193
+ <Text style={productNameStyle} accessibilityRole="header">
194
+ {productName}
195
+ </Text>
196
+ ) : null}
197
+
198
+ {description ? <Text style={descriptionStyle}>{description}</Text> : null}
199
+
200
+ {children ? <>{cloneChildrenWithModes(children, modes)}</> : null}
201
+
202
+ {showStats && (
203
+ <View
204
+ style={{
205
+ flexDirection: 'row',
206
+ alignItems: 'center',
207
+ justifyContent: 'space-between',
208
+ width: '100%',
209
+ }}
210
+ >
211
+ {stats.map((stat, index) => (
212
+ <View
213
+ key={`${stat.label}-${index}`}
214
+ style={{
215
+ flex: 1,
216
+ minWidth: 0,
217
+ alignItems: 'center',
218
+ gap: statGap,
219
+ overflow: 'hidden',
220
+ }}
221
+ >
222
+ <Text style={statValueStyle} numberOfLines={1}>
223
+ {stat.value}
224
+ </Text>
225
+ <Text style={statLabelStyle} numberOfLines={1}>
226
+ {stat.label}
227
+ </Text>
228
+ </View>
229
+ ))}
230
+ </View>
231
+ )}
232
+ </View>
233
+ )
234
+ }
235
+
236
+ export default ProductOverview
@@ -0,0 +1,394 @@
1
+ import React, { useCallback, useState } from 'react'
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native'
3
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
+ import { EMPTY_MODES } from '../../utils/react-utils'
6
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
7
+ import SegmentedTrack, {
8
+ type SegmentedTrackSegmentData,
9
+ } 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
+ export type RangeTrackItem = {
22
+ /** Stable React key. Falls back to the item label / index. */
23
+ key?: React.Key
24
+ /** The descriptive label rendered next to the indicator dot. */
25
+ label?: React.ReactNode
26
+ /**
27
+ * The data point — drives the segment's proportional width. Same
28
+ * semantics as `SegmentedTrackSegmentData.value`. When the legend's
29
+ * right-side text is shown, the resolution order is:
30
+ * `displayValue` → `formatValue(value)` → hide.
31
+ */
32
+ value: number
33
+ /**
34
+ * Optional override for the legend row's right-side text. Falls
35
+ * back to `formatValue(value)` if a parent-level `formatValue` is
36
+ * provided; otherwise the value slot is hidden (matches the
37
+ * `data` boolean toggle on `MetricLegendItem`). Pass `null` or
38
+ * `false` to explicitly hide on a per-item basis.
39
+ */
40
+ displayValue?: React.ReactNode
41
+ /**
42
+ * Hard-override the shared color used for both the segment and the
43
+ * legend indicator. Bypasses `dataViz/bg` token resolution.
44
+ */
45
+ color?: string
46
+ /**
47
+ * Per-item design token mode overrides. Merged on top of parent
48
+ * `modes` and the per-index `Emphasis / DataViz` defaults
49
+ * (`High`, `Medium`, `Low`, then cycles).
50
+ */
51
+ modes?: Record<string, any>
52
+ /** Accessibility label for the segment + legend row pairing. */
53
+ accessibilityLabel?: string
54
+ }
55
+
56
+ /**
57
+ * One tab inside a `RangeTrack`. Each tab carries its own `items`
58
+ * array, which is the single source of truth for both the
59
+ * `SegmentedTrack` segments AND the `MetricLegendItem` legend rows
60
+ * rendered for that tab.
61
+ */
62
+ export type RangeTrackTab = {
63
+ /** Stable React key. Falls back to the tab label / index. */
64
+ key?: React.Key
65
+ /** The label shown inside the `TabItem` at the top of the component. */
66
+ label: string
67
+ /**
68
+ * Data-driven rows. Each entry produces one `SegmentedTrack`
69
+ * segment AND one matching `MetricLegendItem` legend row — the
70
+ * count of legend rows is locked in step with the count of segments
71
+ * by construction.
72
+ */
73
+ items: RangeTrackItem[]
74
+ /**
75
+ * Per-tab accessibility label for the tab item button. Falls back
76
+ * to `label`.
77
+ */
78
+ accessibilityLabel?: string
79
+ }
80
+
81
+ export type RangeTrackProps = {
82
+ /**
83
+ * Tabs to render across the top. Each tab carries its own `items`
84
+ * array, which is the single source of truth for that tab's
85
+ * segments and legend rows. Switching tabs swaps both the segments
86
+ * and the legend together.
87
+ */
88
+ tabs: RangeTrackTab[]
89
+ /**
90
+ * Optional formatter applied to each item's numeric `value` when
91
+ * the item does not provide an explicit `displayValue`. Useful for
92
+ * the common "format every weight the same way" case
93
+ * (e.g. `(v) => `${v}%``). Applies to the legend rows of the
94
+ * currently active tab.
95
+ */
96
+ formatValue?: (value: number) => React.ReactNode
97
+ /**
98
+ * Controlled active tab index. When provided, the component is
99
+ * fully controlled and `onTabChange` should be wired up. When
100
+ * `undefined`, the component manages the active tab internally
101
+ * starting from `defaultActiveTabIndex`.
102
+ */
103
+ activeTabIndex?: number
104
+ /**
105
+ * Initial active tab index when the component is uncontrolled.
106
+ * Ignored if `activeTabIndex` is provided.
107
+ * @default 0
108
+ */
109
+ defaultActiveTabIndex?: number
110
+ /**
111
+ * Fired when the user taps a tab. Receives the new index and the
112
+ * matching `RangeTrackTab` from the `tabs` array.
113
+ */
114
+ onTabChange?: (index: number, tab: RangeTrackTab) => void
115
+ /**
116
+ * When true, the tab row scrolls horizontally — useful when there
117
+ * are many tabs.
118
+ * @default false
119
+ */
120
+ scrollableTabs?: boolean
121
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
122
+ modes?: Record<string, any>
123
+ /** Override the outer container styles. */
124
+ style?: StyleProp<ViewStyle>
125
+ /** Override the tab row styles. */
126
+ tabsStyle?: StyleProp<ViewStyle>
127
+ /** Override the `SegmentedTrack` styles. */
128
+ trackStyle?: StyleProp<ViewStyle>
129
+ /** Override the legend list container styles. */
130
+ legendStyle?: StyleProp<ViewStyle>
131
+ /** Accessibility label announced for the whole component. */
132
+ accessibilityLabel?: string
133
+ }
134
+
135
+ const DEFAULT_EMPHASIS_CYCLE = ['High', 'Medium', 'Low'] as const
136
+
137
+ const DEFAULT_TABS: RangeTrackTab[] = [
138
+ {
139
+ label: 'Tab item',
140
+ items: [
141
+ { label: 'Large cap', value: 1, displayValue: '25%' },
142
+ { label: 'Mid cap', value: 1, displayValue: '25%' },
143
+ { label: 'Small cap', value: 1, displayValue: '25%' },
144
+ ],
145
+ },
146
+ {
147
+ label: 'Tab item',
148
+ items: [
149
+ { label: 'Large cap', value: 1, displayValue: '25%' },
150
+ { label: 'Mid cap', value: 1, displayValue: '25%' },
151
+ { label: 'Small cap', value: 1, displayValue: '25%' },
152
+ ],
153
+ },
154
+ {
155
+ label: 'Tab item',
156
+ items: [
157
+ { label: 'Large cap', value: 1, displayValue: '25%' },
158
+ { label: 'Mid cap', value: 1, displayValue: '25%' },
159
+ { label: 'Small cap', value: 1, displayValue: '25%' },
160
+ ],
161
+ },
162
+ ]
163
+
164
+ const defaultEmphasisFor = (index: number) =>
165
+ DEFAULT_EMPHASIS_CYCLE[index % DEFAULT_EMPHASIS_CYCLE.length]
166
+
167
+ /**
168
+ * Resolve the shared color for an item. Honors any explicit `color`
169
+ * override, then falls back to `dataViz/bg` for the merged mode set,
170
+ * then to the Figma reference value.
171
+ *
172
+ * Mirrors the cascade used by `SegmentedTrack` (per-index `Emphasis /
173
+ * DataViz` defaults of `High` → `Medium` → `Low`, cycling) so the
174
+ * pre-computed color we pass to both the segment and the legend
175
+ * indicator matches what the `SegmentedTrack` would have computed on
176
+ * its own. This is what keeps segments and legend rows in sync by
177
+ * construction.
178
+ */
179
+ function resolveItemColor(
180
+ parentModes: Record<string, any>,
181
+ item: RangeTrackItem,
182
+ index: number
183
+ ): string {
184
+ if (item.color) return item.color
185
+ const itemModes = {
186
+ ...parentModes,
187
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
188
+ ...(item.modes || {}),
189
+ }
190
+ return (
191
+ (getVariableByName('dataViz/bg', itemModes) as string | null) ?? '#cea15a'
192
+ )
193
+ }
194
+
195
+ /**
196
+ * Resolve what to render in the legend row's right-side slot. The
197
+ * order of precedence is:
198
+ * 1. `item.displayValue` if explicitly provided (including `null` /
199
+ * `false`, which the underlying `MetricLegendItem` treats as
200
+ * "hide the value slot").
201
+ * 2. `formatValue(item.value)` when a parent-level formatter exists.
202
+ * 3. `undefined` — the value slot is hidden.
203
+ */
204
+ function resolveLegendValue(
205
+ item: RangeTrackItem,
206
+ formatValue: ((value: number) => React.ReactNode) | undefined
207
+ ): React.ReactNode {
208
+ if (item.displayValue !== undefined) return item.displayValue
209
+ if (formatValue) return formatValue(item.value)
210
+ return undefined
211
+ }
212
+
213
+ /**
214
+ * `RangeTrack` pairs a tab row with a `SegmentedTrack` and a vertical
215
+ * stack of `MetricLegendItem` rows. Each tab carries its own `items`
216
+ * array which is the **single source of truth** for both the segments
217
+ * and the legend rows of that tab — every segment has exactly one
218
+ * legend row and they share the same color through the same
219
+ * `Emphasis / DataViz` cascade as the standalone `SegmentedTrack`.
220
+ *
221
+ * Switching tabs swaps the segments and the legend together so the
222
+ * two visualizations can never drift out of sync.
223
+ *
224
+ * The default 3-item layout per tab receives per-index
225
+ * `Emphasis / DataViz` defaults (item 1 → `High`, 2 → `Medium`, 3 →
226
+ * `Low`, then cycles). Override `Appearance / DataViz` on the parent
227
+ * `modes` to retheme the whole component, or set `modes` per item to
228
+ * remix.
229
+ *
230
+ * The component supports both **controlled** and **uncontrolled**
231
+ * tab selection — pass `activeTabIndex` + `onTabChange` for the
232
+ * controlled mode, or omit them and the component will manage the
233
+ * selection internally starting from `defaultActiveTabIndex`.
234
+ *
235
+ * @component
236
+ * @param {RangeTrackProps} props
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * <RangeTrack
241
+ * tabs={[
242
+ * {
243
+ * label: 'Sectoral',
244
+ * items: [
245
+ * { label: 'Large cap', value: 50, displayValue: '50%' },
246
+ * { label: 'Mid cap', value: 30, displayValue: '30%' },
247
+ * { label: 'Small cap', value: 20, displayValue: '20%' },
248
+ * ],
249
+ * },
250
+ * {
251
+ * label: 'Geography',
252
+ * items: [
253
+ * { label: 'India', value: 70, displayValue: '70%' },
254
+ * { label: 'US', value: 20, displayValue: '20%' },
255
+ * { label: 'Other', value: 10, displayValue: '10%' },
256
+ * ],
257
+ * },
258
+ * ]}
259
+ * />
260
+ * ```
261
+ */
262
+ function RangeTrack({
263
+ tabs,
264
+ formatValue,
265
+ activeTabIndex,
266
+ defaultActiveTabIndex = 0,
267
+ onTabChange,
268
+ scrollableTabs = false,
269
+ modes: propModes = EMPTY_MODES,
270
+ style,
271
+ tabsStyle,
272
+ trackStyle,
273
+ legendStyle,
274
+ accessibilityLabel,
275
+ }: RangeTrackProps) {
276
+ const { modes: globalModes } = useTokens()
277
+ const modes = { ...globalModes, ...propModes }
278
+
279
+ const resolvedTabs = tabs && tabs.length > 0 ? tabs : DEFAULT_TABS
280
+
281
+ const [internalIndex, setInternalIndex] = useState(() =>
282
+ clampIndex(defaultActiveTabIndex, resolvedTabs.length)
283
+ )
284
+
285
+ const isControlled = activeTabIndex !== undefined
286
+ const rawIndex = isControlled ? activeTabIndex : internalIndex
287
+ const safeIndex = clampIndex(rawIndex, resolvedTabs.length)
288
+
289
+ const handleTabPress = useCallback(
290
+ (index: number) => {
291
+ const nextTab = resolvedTabs[index]
292
+ if (!nextTab) return
293
+ if (!isControlled) setInternalIndex(index)
294
+ onTabChange?.(index, nextTab)
295
+ },
296
+ [isControlled, onTabChange, resolvedTabs]
297
+ )
298
+
299
+ const containerGap =
300
+ (getVariableByName('rangeTrack/gap', modes) as number | null) ?? 28
301
+ // Vertical gap between legend rows is not exposed as its own token —
302
+ // the Figma design uses 8px between rows, mirroring the
303
+ // `donutChartSummary/legend/gap` default. Keep the value in step
304
+ // with `DonutChartSummary` so the two summary components feel
305
+ // cohesive when stacked.
306
+ const legendRowGap = 8
307
+
308
+ const activeTab = resolvedTabs[safeIndex]
309
+ const activeItems = activeTab?.items ?? []
310
+
311
+ const segments: SegmentedTrackSegmentData[] = activeItems.map(
312
+ (item, index) => ({
313
+ key: item.key ?? `segment-${index}`,
314
+ value: item.value,
315
+ color: resolveItemColor(modes, item, index),
316
+ accessibilityLabel: item.accessibilityLabel,
317
+ })
318
+ )
319
+
320
+ return (
321
+ <View
322
+ accessibilityRole="summary"
323
+ accessibilityLabel={accessibilityLabel}
324
+ style={[
325
+ {
326
+ width: '100%',
327
+ flexDirection: 'column',
328
+ alignItems: 'flex-start',
329
+ gap: containerGap,
330
+ },
331
+ style,
332
+ ]}
333
+ >
334
+ <Tabs
335
+ modes={modes}
336
+ scrollable={scrollableTabs}
337
+ style={tabsStyle}
338
+ >
339
+ {resolvedTabs.map((tab, index) => (
340
+ <TabItem
341
+ key={tab.key ?? tab.label ?? `tab-${index}`}
342
+ label={tab.label}
343
+ active={index === safeIndex}
344
+ onPress={() => handleTabPress(index)}
345
+ accessibilityLabel={tab.accessibilityLabel ?? tab.label}
346
+ />
347
+ ))}
348
+ </Tabs>
349
+
350
+ <SegmentedTrack
351
+ modes={modes}
352
+ segments={segments}
353
+ style={trackStyle}
354
+ accessibilityLabel={activeTab?.accessibilityLabel ?? activeTab?.label}
355
+ />
356
+
357
+ <View
358
+ style={[
359
+ {
360
+ width: '100%',
361
+ flexDirection: 'column',
362
+ alignItems: 'flex-start',
363
+ gap: legendRowGap,
364
+ },
365
+ legendStyle,
366
+ ]}
367
+ >
368
+ {activeItems.map((item, index) => (
369
+ <MetricLegendItem
370
+ key={item.key ?? `legend-${index}`}
371
+ label={item.label}
372
+ value={resolveLegendValue(item, formatValue)}
373
+ indicatorColor={resolveItemColor(modes, item, index)}
374
+ modes={modes}
375
+ />
376
+ ))}
377
+ </View>
378
+ </View>
379
+ )
380
+ }
381
+
382
+ /**
383
+ * Clamp a tab index into `[0, length)`. Negative or out-of-bounds
384
+ * values fall back to `0` to avoid rendering an undefined tab.
385
+ */
386
+ function clampIndex(index: number, length: number): number {
387
+ if (length <= 0) return 0
388
+ if (!Number.isFinite(index)) return 0
389
+ if (index < 0) return 0
390
+ if (index >= length) return length - 1
391
+ return Math.floor(index)
392
+ }
393
+
394
+ export default RangeTrack