jfs-components 0.0.84 → 0.0.86

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
  3. package/lib/commonjs/components/AppBar/AppBar.js +36 -22
  4. package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
  5. package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
  6. package/lib/commonjs/components/Attached/Attached.js +34 -4
  7. package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
  8. package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
  9. package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +52 -89
  11. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
  12. package/lib/commonjs/components/index.js +34 -0
  13. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
  16. package/lib/module/components/AppBar/AppBar.js +36 -22
  17. package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
  18. package/lib/module/components/AreaLineChart/chartMath.js +242 -0
  19. package/lib/module/components/Attached/Attached.js +34 -4
  20. package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
  21. package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
  22. package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
  23. package/lib/module/components/FullscreenModal/FullscreenModal.js +53 -90
  24. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
  25. package/lib/module/components/index.js +4 -0
  26. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  27. package/lib/module/icons/registry.js +1 -1
  28. package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
  29. package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
  30. package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
  31. package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
  32. package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
  33. package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
  34. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +21 -25
  35. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
  36. package/lib/typescript/src/components/index.d.ts +4 -0
  37. package/lib/typescript/src/icons/registry.d.ts +1 -1
  38. package/package.json +1 -1
  39. package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
  40. package/src/components/AppBar/AppBar.tsx +37 -24
  41. package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
  42. package/src/components/AreaLineChart/chartMath.ts +265 -0
  43. package/src/components/Attached/Attached.tsx +36 -5
  44. package/src/components/BubbleChart/BubbleChart.tsx +319 -0
  45. package/src/components/BubbleChart/bubblePacking.ts +397 -0
  46. package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
  47. package/src/components/FullscreenModal/FullscreenModal.tsx +61 -119
  48. package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
  49. package/src/components/index.ts +4 -0
  50. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  51. package/src/icons/registry.ts +1 -1
@@ -10,23 +10,20 @@ export type FullscreenModalProps = {
10
10
  /** Secondary line below the supporting paragraph (e.g. a price / timeline). */
11
11
  priceText?: string;
12
12
  /**
13
- * Media rendered full-bleed behind the hero text and driven by the parallax
14
- * scroll effect. Bring any renderer — most commonly a `LottiePlayer`, but an
15
- * `Image`, `Video`, or `SvgXml` works too. Size it to fill the hero box
16
- * (`heroHeight` tall, full modal width) and let it `cover` so that as the
17
- * hero collapses in height the art is cropped, never distorted. `modes` are
18
- * cascaded into it.
13
+ * Background media rendered full-bleed behind the hero text. Bring any
14
+ * renderer — most commonly an `Image`, but a `LottiePlayer`, `Video`, or
15
+ * `SvgXml` works too. It is laid out at the full modal width; size it with an
16
+ * aspect ratio (e.g. `<Image ratio={3 / 4} />`) so its height follows the
17
+ * width naturally. The media scrolls together with the rest of the content
18
+ * (no parallax). `modes` are cascaded into it.
19
19
  */
20
20
  heroMedia?: React.ReactNode;
21
- /** Resting height of the hero region. Defaults to 420. */
22
- heroHeight?: number;
23
21
  /**
24
- * Collapsed height the hero shrinks to at full scroll. Defaults to
25
- * `heroHeight * 0.45`. Only the height changes the width is always full.
22
+ * Fallback height for the hero text region when no `heroMedia` is provided.
23
+ * When `heroMedia` is present, the hero height is driven entirely by the
24
+ * media's own aspect ratio and this value is ignored. Defaults to 420.
26
25
  */
27
- heroMinHeight?: number;
28
- /** Enable the scroll-driven hero collapse. Defaults to true. */
29
- parallax?: boolean;
26
+ heroHeight?: number;
30
27
  /** Whether to render the floating close button (top-right). Defaults to true. */
31
28
  showClose?: boolean;
32
29
  /** Press handler for the close button. */
@@ -57,8 +54,9 @@ export type FullscreenModalProps = {
57
54
  testID?: string;
58
55
  };
59
56
  /**
60
- * FullscreenModal — a full-screen takeover surface with a parallax media hero,
61
- * a scrollable body, a floating close button, and a sticky `ActionFooter`.
57
+ * FullscreenModal — a full-screen takeover surface with a full-bleed media
58
+ * hero, a scrollable body, a floating close button, and a sticky
59
+ * `ActionFooter`.
62
60
  *
63
61
  * The component always themes itself with `context5: 'Fullscreen Modal'`
64
62
  * (non-overridable) so every nested component (Section, ListItem, Button,
@@ -66,14 +64,12 @@ export type FullscreenModalProps = {
66
64
  * That mode is cascaded into `children`, the footer, and the hero text via
67
65
  * `cloneChildrenWithModes` / the merged `modes` object.
68
66
  *
69
- * ### Parallax
70
- * As the user scrolls up, the hero collapses by **height only** (from
71
- * `heroHeight` to `heroMinHeight`) its **full width is always preserved**.
72
- * The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
73
- * the collapsing clip, so it keeps a perfect aspect ratio the whole time
74
- * (never scaled or squished). Because it collapses slower than the content
75
- * scrolls, the media lags behind for the parallax depth cue. Disable with
76
- * `parallax={false}`.
67
+ * ### Hero
68
+ * The `heroMedia` is rendered full modal width inside the scroll body and
69
+ * takes its height from its own aspect ratio. The hero text (eyebrow /
70
+ * headline / supporting / price) is overlaid on top, anchored to the bottom.
71
+ * The whole hero scrolls together with the rest of the content — there is no
72
+ * parallax effect.
77
73
  *
78
74
  * @component
79
75
  * @example
@@ -83,7 +79,7 @@ export type FullscreenModalProps = {
83
79
  * headline="Get more from your money."
84
80
  * supportingText="JioFinance+ is your upgraded financial experience…"
85
81
  * priceText="₹999/year · ₹0 until 2027"
86
- * heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
82
+ * heroMedia={<Image imageSource={hero} ratio={3 / 4} />}
87
83
  * primaryActionLabel="Upgrade for free"
88
84
  * disclaimer="By upgrading, we'll check your eligibility with Experian."
89
85
  * onPrimaryAction={() => upgrade()}
@@ -94,6 +90,6 @@ export type FullscreenModalProps = {
94
90
  * </FullscreenModal>
95
91
  * ```
96
92
  */
97
- declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, heroMinHeight, parallax, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, backgroundColor, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
93
+ declare function FullscreenModal({ eyebrow, headline, supportingText, priceText, heroMedia, heroHeight, showClose, onClose, closeAccessibilityLabel, footer, primaryActionLabel, onPrimaryAction, disclaimer, backgroundColor, children, modes: propModes, style, contentContainerStyle, testID, }: FullscreenModalProps): import("react/jsx-runtime").JSX.Element;
98
94
  export default FullscreenModal;
99
95
  //# sourceMappingURL=FullscreenModal.d.ts.map
@@ -13,6 +13,12 @@ export type MetricLegendItemProps = {
13
13
  * `metricLegendItem/indicator/bg` design token.
14
14
  */
15
15
  indicatorColor?: string;
16
+ /**
17
+ * Shape of the leading indicator. `'dot'` (default) renders the small
18
+ * circle used in categorical legends; `'line'` renders a short
19
+ * horizontal bar, matching the legend of a line chart.
20
+ */
21
+ indicatorShape?: 'dot' | 'line';
16
22
  /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
17
23
  modes?: Record<string, any>;
18
24
  /** Override container styles. */
@@ -32,6 +38,6 @@ export type MetricLegendItemProps = {
32
38
  * @component
33
39
  * @param {MetricLegendItemProps} props
34
40
  */
35
- declare function MetricLegendItem({ label, value, indicatorColor, modes, style, indicatorStyle, labelStyle, valueStyle, }: MetricLegendItemProps): import("react/jsx-runtime").JSX.Element;
41
+ declare function MetricLegendItem({ label, value, indicatorColor, indicatorShape, modes, style, indicatorStyle, labelStyle, valueStyle, }: MetricLegendItemProps): import("react/jsx-runtime").JSX.Element;
36
42
  export default MetricLegendItem;
37
43
  //# sourceMappingURL=MetricLegendItem.d.ts.map
@@ -38,6 +38,7 @@ export { default as CircularProgressBarDoted, type CircularProgressBarDotedProps
38
38
  export { default as CircularRating, type CircularRatingProps } from './CircularRating/CircularRating';
39
39
  export { default as CoverageRing, type CoverageRingProps } from './CoverageRing/CoverageRing';
40
40
  export { default as CoverageBarComparison, type CoverageBarComparisonProps, type CoverageBarComparisonItem } from './CoverageBarComparison/CoverageBarComparison';
41
+ export { default as AllocationComparisonChart, type AllocationComparisonChartProps, type AllocationSegment } from './AllocationComparisonChart/AllocationComparisonChart';
41
42
  export { default as MonthlyStatusGrid, CalendarGlyph, type MonthlyStatusGridProps, type MonthlyStatusGridMonth, type MonthlyStatus, type CalendarGlyphProps } from './MonthlyStatusGrid/MonthlyStatusGrid';
42
43
  export { default as Gauge, type GaugeProps } from './Gauge/Gauge';
43
44
  export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/HoldingsCard';
@@ -101,6 +102,9 @@ export { default as RadioButton, type RadioButtonProps } from './RadioButton/Rad
101
102
  export { default as RechargeCard, type RechargeCardProps } from './RechargeCard/RechargeCard';
102
103
  export { default as SavingsGoalSummary, type SavingsGoalSummaryProps, type SavingsGoalSummaryItem } from './SavingsGoalSummary/SavingsGoalSummary';
103
104
  export { default as DonutChart, type DonutChartProps, type DonutChartSegmentData, type DonutChartSegmentProps, DonutChartSegment } from './DonutChart/DonutChart';
105
+ export { default as AreaLineChart, useChart, type AreaLineChartProps, type ChartSeries, type ChartPoint, type ChartInset, type GoalPinConfig, type ChartGridProps, type ChartXAxisProps, type ChartYAxisProps, type ChartGoalPinProps } from './AreaLineChart/AreaLineChart';
106
+ export { default as ClusterBubble, type ClusterBubbleProps, type ClusterBubbleLabelPlacement, type ClusterBubbleLabelDirection } from './ClusterBubble/ClusterBubble';
107
+ export { default as BubbleChart, type BubbleChartProps, type BubbleDatum } from './BubbleChart/BubbleChart';
104
108
  export { default as DonutChartSummary, type DonutChartSummaryProps, type DonutChartSummaryItem } from './DonutChartSummary/DonutChartSummary';
105
109
  export { default as RangeTrack, type RangeTrackProps, type RangeTrackTab, type RangeTrackItem } from './RangeTrack/RangeTrack';
106
110
  export { default as SegmentedTrack, type SegmentedTrackProps, type SegmentedTrackSegmentData, type SegmentedTrackSegmentProps, SegmentedTrackSegment } from './SegmentedTrack/SegmentedTrack';
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-06-01T15:33:15.385Z
7
+ * Generated: 2026-06-03T15:59:02.370Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -0,0 +1,450 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from 'react-native'
9
+ import Svg, { Line } from 'react-native-svg'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
12
+ import { EMPTY_MODES } from '../../utils/react-utils'
13
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
14
+
15
+ /**
16
+ * One vertical pill in the {@link AllocationComparisonChartProps.data} array.
17
+ *
18
+ * Each segment renders a single bar whose **height encodes `value`** (the
19
+ * "current" reading) and, when supplied, a **`baseline`** overlay drawn from
20
+ * the bottom up with a dashed marker line (the "recommended" reading). Both
21
+ * are measured against the same shared scale so bars and baselines are
22
+ * directly comparable across segments.
23
+ */
24
+ export type AllocationSegment = {
25
+ /** Stable React key (falls back to the array index). */
26
+ key?: React.Key
27
+ /** Caption rendered under the bar — e.g. `"Small & Mid"`. */
28
+ label: React.ReactNode
29
+ /**
30
+ * Primary value driving the bar's height (the "current" reading). Scaled
31
+ * against {@link AllocationComparisonChartProps.max}.
32
+ */
33
+ value: number
34
+ /**
35
+ * Optional comparison value (the "recommended" reading). When provided, a
36
+ * filled overlay is drawn from the bottom of the bar up to this level.
37
+ * Omit to render a plain bar.
38
+ */
39
+ baseline?: number
40
+ /**
41
+ * Whether to draw the dashed reference line + value label at the top of the
42
+ * `baseline` overlay. Defaults to `true` **only for the first segment that
43
+ * has a `baseline`** and `false` for the rest, so the marker stays a
44
+ * focused callout rather than repeating on every bar. Set explicitly to
45
+ * force it on/off per segment. Has no effect without a `baseline`.
46
+ */
47
+ showMarker?: boolean
48
+ /**
49
+ * Text shown above the bar. Defaults to `formatValue(value)`. Pass `null`
50
+ * to hide it.
51
+ */
52
+ valueLabel?: React.ReactNode
53
+ /**
54
+ * Text shown beside the dashed baseline marker. Defaults to
55
+ * `formatValue(baseline)`. Pass `null` to hide just the marker label while
56
+ * keeping the dashed line.
57
+ */
58
+ baselineLabel?: React.ReactNode
59
+ /** Hard-override the bar (current) fill color. */
60
+ color?: string
61
+ /** Hard-override the baseline (recommended) overlay color. */
62
+ baselineColor?: string
63
+ /** Per-segment accessibility label. */
64
+ accessibilityLabel?: string
65
+ }
66
+
67
+ export type AllocationComparisonChartProps = {
68
+ /**
69
+ * The segments to plot, left → right. Each carries its `value`, optional
70
+ * `baseline` and its `label`, so a bar can never drift from its caption.
71
+ */
72
+ data?: AllocationSegment[]
73
+ /**
74
+ * Maximum value used to scale every bar **and** baseline. Defaults to the
75
+ * largest `value`/`baseline` across `data`. Pass an explicit `max` (e.g.
76
+ * `100` for percentages) for a fixed scale.
77
+ */
78
+ max?: number
79
+ /**
80
+ * Height in px of the bar area — i.e. the height of a bar whose value
81
+ * equals `max`. Excludes the value/caption label rows. Default: `154`.
82
+ */
83
+ height?: number
84
+ /** Width of each pill bar in px. Default: the `segmentIndicator/track/width` token (`28`). */
85
+ barWidth?: number
86
+ /** Show the legend row above the chart. Default: `true`. */
87
+ showLegend?: boolean
88
+ /** Legend label for the bar (current) series. Default: `"Current"`. */
89
+ valueLegendLabel?: React.ReactNode
90
+ /**
91
+ * Legend label for the baseline (recommended) series. Default:
92
+ * `"Recommended"`. The baseline legend item only appears when at least one
93
+ * segment defines a `baseline`.
94
+ */
95
+ baselineLegendLabel?: React.ReactNode
96
+ /**
97
+ * Formats numeric `value`/`baseline` into the default labels. Default:
98
+ * `(v) => \`${v}%\``.
99
+ */
100
+ formatValue?: (value: number) => string
101
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
102
+ modes?: Record<string, any>
103
+ /** Container style override. */
104
+ style?: StyleProp<ViewStyle>
105
+ /** Style applied to the bars row. */
106
+ chartStyle?: StyleProp<ViewStyle>
107
+ /** Style applied to the legend row. */
108
+ legendStyle?: StyleProp<ViewStyle>
109
+ /** Accessibility label for the whole chart. */
110
+ accessibilityLabel?: string
111
+ }
112
+
113
+ const DEFAULT_DATA: AllocationSegment[] = [
114
+ { label: 'Small & Mid', value: 65, baseline: 35 },
115
+ { label: 'Large', value: 25 },
116
+ { label: 'Others', value: 10 },
117
+ ]
118
+
119
+ const toNumber = (value: unknown, fallback: number): number => {
120
+ if (typeof value === 'number') {
121
+ return Number.isFinite(value) ? value : fallback
122
+ }
123
+ if (typeof value === 'string') {
124
+ const parsed = Number(value)
125
+ return Number.isFinite(parsed) ? parsed : fallback
126
+ }
127
+ return fallback
128
+ }
129
+
130
+ const toFontWeight = (
131
+ value: unknown,
132
+ fallback: TextStyle['fontWeight']
133
+ ): TextStyle['fontWeight'] => {
134
+ if (typeof value === 'number') {
135
+ return String(value) as TextStyle['fontWeight']
136
+ }
137
+ if (typeof value === 'string') {
138
+ return value as TextStyle['fontWeight']
139
+ }
140
+ return fallback
141
+ }
142
+
143
+ const isShown = (node: React.ReactNode): boolean =>
144
+ node !== undefined && node !== null && node !== false
145
+
146
+ type SegmentTheme = {
147
+ barWidth: number
148
+ pillRadius: number
149
+ gap: number
150
+ currentColor: string
151
+ baselineColor: string
152
+ lineColor: string
153
+ lineSize: number
154
+ labelStyle: TextStyle
155
+ }
156
+
157
+ type SegmentBarProps = {
158
+ segment: AllocationSegment
159
+ barHeightPx: number
160
+ baselineHeightPx: number | null
161
+ baselineLabel: React.ReactNode
162
+ showMarker: boolean
163
+ theme: SegmentTheme
164
+ }
165
+
166
+ /**
167
+ * Internal: one vertical pill column (the Figma "Segment Indicator"). Not
168
+ * exported — the ergonomic public unit is the chart driven by `data`. The
169
+ * `segmentIndicator/*` token names are mirrored here so design ↔ code token
170
+ * alignment is preserved.
171
+ */
172
+ function SegmentBar({
173
+ segment,
174
+ barHeightPx,
175
+ baselineHeightPx,
176
+ baselineLabel,
177
+ showMarker,
178
+ theme,
179
+ }: SegmentBarProps) {
180
+ const { barWidth, pillRadius, gap, currentColor, baselineColor, lineColor, lineSize, labelStyle } =
181
+ theme
182
+
183
+ const fillColor = segment.color ?? currentColor
184
+ const overlayColor = segment.baselineColor ?? baselineColor
185
+ const showValueLabel = isShown(segment.valueLabel)
186
+
187
+ const hasBaseline = baselineHeightPx !== null && baselineHeightPx > 0
188
+ const overlayHeight = hasBaseline ? Math.min(baselineHeightPx as number, barHeightPx) : 0
189
+ const overlayRadius = Math.min(pillRadius, barWidth / 2, overlayHeight / 2)
190
+
191
+ return (
192
+ <View
193
+ style={{ flex: 1, alignItems: 'center', justifyContent: 'flex-end', gap }}
194
+ accessibilityLabel={segment.accessibilityLabel}
195
+ >
196
+ {showValueLabel ? (
197
+ <Text numberOfLines={1} style={labelStyle}>
198
+ {segment.valueLabel}
199
+ </Text>
200
+ ) : null}
201
+
202
+ <View
203
+ style={{
204
+ width: barWidth,
205
+ height: Math.max(barHeightPx, 1),
206
+ borderRadius: pillRadius,
207
+ backgroundColor: fillColor,
208
+ position: 'relative',
209
+ }}
210
+ >
211
+ {hasBaseline ? (
212
+ <>
213
+ <View
214
+ style={{
215
+ position: 'absolute',
216
+ left: 0,
217
+ right: 0,
218
+ bottom: 0,
219
+ height: overlayHeight,
220
+ backgroundColor: overlayColor,
221
+ borderBottomLeftRadius: overlayRadius,
222
+ borderBottomRightRadius: overlayRadius,
223
+ }}
224
+ />
225
+ {showMarker ? (
226
+ <View
227
+ style={{
228
+ position: 'absolute',
229
+ left: 0,
230
+ bottom: overlayHeight,
231
+ height: 0,
232
+ flexDirection: 'row',
233
+ alignItems: 'center',
234
+ }}
235
+ pointerEvents="none"
236
+ >
237
+ <Svg width={barWidth} height={Math.max(lineSize, 1)}>
238
+ <Line
239
+ x1={0}
240
+ y1={Math.max(lineSize, 1) / 2}
241
+ x2={barWidth}
242
+ y2={Math.max(lineSize, 1) / 2}
243
+ stroke={lineColor}
244
+ strokeWidth={lineSize}
245
+ strokeDasharray="2 2"
246
+ />
247
+ </Svg>
248
+ {isShown(baselineLabel) ? (
249
+ <Text numberOfLines={1} style={[labelStyle, { marginLeft: 6 }]}>
250
+ {baselineLabel}
251
+ </Text>
252
+ ) : null}
253
+ </View>
254
+ ) : null}
255
+ </>
256
+ ) : null}
257
+ </View>
258
+
259
+ <Text numberOfLines={1} style={labelStyle}>
260
+ {segment.label}
261
+ </Text>
262
+ </View>
263
+ )
264
+ }
265
+
266
+ /**
267
+ * `AllocationComparisonChart` plots a row of vertical pill bars that compare a
268
+ * **current** reading (each bar's height) against an optional **recommended**
269
+ * baseline (a filled overlay drawn from the bottom up, marked with a dashed
270
+ * line). Every bar and baseline shares a single scale, so heights are directly
271
+ * comparable across segments — no axes required.
272
+ *
273
+ * The chart is driven entirely by the `data` array: each entry pairs a
274
+ * `value`, an optional `baseline` and its `label`, so a bar can never drift
275
+ * out of sync with its caption or its baseline marker.
276
+ *
277
+ * Colors, fonts, spacing and the pill radius resolve from the Figma
278
+ * `segmentIndicator/*`, `metricLegendItem/*` and `allocationComparisonChart/*`
279
+ * tokens via the `modes` prop.
280
+ *
281
+ * @component
282
+ */
283
+ function AllocationComparisonChart({
284
+ data = DEFAULT_DATA,
285
+ max,
286
+ height = 154,
287
+ barWidth,
288
+ showLegend = true,
289
+ valueLegendLabel = 'Current',
290
+ baselineLegendLabel = 'Recommended',
291
+ formatValue = (value: number) => `${value}%`,
292
+ modes: propModes = EMPTY_MODES,
293
+ style,
294
+ chartStyle,
295
+ legendStyle,
296
+ accessibilityLabel,
297
+ }: AllocationComparisonChartProps) {
298
+ const { modes: globalModes } = useTokens()
299
+ const modes = React.useMemo(
300
+ () => ({ ...globalModes, ...propModes }),
301
+ [globalModes, propModes]
302
+ )
303
+
304
+ const trackWidth = toNumber(getVariableByName('segmentIndicator/track/width', modes), 28)
305
+ const resolvedBarWidth = barWidth ?? trackWidth
306
+ const radiusToken = toNumber(getVariableByName('segmentIndicator/indicator/radius', modes), 99999)
307
+ const pillRadius = Math.min(radiusToken, resolvedBarWidth / 2)
308
+ const gap = toNumber(getVariableByName('segmentIndicator/gap', modes), 4)
309
+ const chartGap = toNumber(getVariableByName('allocationComparisonChart/gap', modes), 8)
310
+
311
+ const currentColor =
312
+ (getVariableByName('segmentIndicator/indicator/background', modes) as string | null) ??
313
+ '#5d00b5'
314
+ const baselineColor =
315
+ (getVariableByName('segmentIndicator/indicator/foreground', modes) as string | null) ??
316
+ '#b84fbd'
317
+ const lineColor =
318
+ (getVariableByName('segmentIndicator/indicator/line/color', modes) as string | null) ??
319
+ '#ffffff'
320
+ const lineSize = toNumber(getVariableByName('segmentIndicator/indicator/line/size', modes), 1)
321
+
322
+ const foreground =
323
+ (getVariableByName('segmentIndicator/foreground', modes) as string | null) ?? '#0c0d10'
324
+ const fontFamily =
325
+ (getVariableByName('segmentIndicator/fontFamily', modes) as string | null) ?? 'JioType Var'
326
+ const fontSize = toNumber(getVariableByName('segmentIndicator/fontSize', modes), 12)
327
+ const lineHeight = toNumber(getVariableByName('segmentIndicator/lineHeight', modes), 12)
328
+ const fontWeight = toFontWeight(getVariableByName('segmentIndicator/fontWeight', modes), '400')
329
+
330
+ const labelStyle: TextStyle = {
331
+ color: foreground,
332
+ fontFamily,
333
+ fontSize,
334
+ lineHeight,
335
+ fontWeight,
336
+ textAlign: 'center',
337
+ }
338
+
339
+ const computedMax =
340
+ max ??
341
+ data.reduce(
342
+ (acc, seg) => Math.max(acc, seg.value, seg.baseline ?? 0),
343
+ 0
344
+ )
345
+ const safeMax = computedMax > 0 ? computedMax : 1
346
+
347
+ const firstBaselineIndex = data.findIndex(
348
+ (seg) => typeof seg.baseline === 'number'
349
+ )
350
+ const hasAnyBaseline = firstBaselineIndex !== -1
351
+
352
+ const theme: SegmentTheme = {
353
+ barWidth: resolvedBarWidth,
354
+ pillRadius,
355
+ gap,
356
+ currentColor,
357
+ baselineColor,
358
+ lineColor,
359
+ lineSize,
360
+ labelStyle,
361
+ }
362
+
363
+ const defaultAccessibilityLabel =
364
+ accessibilityLabel ??
365
+ `Allocation comparison of ${data.length} segment${data.length === 1 ? '' : 's'}: ` +
366
+ data
367
+ .map((seg) => {
368
+ const label = typeof seg.label === 'string' ? seg.label : 'segment'
369
+ const base =
370
+ typeof seg.baseline === 'number'
371
+ ? `, recommended ${seg.baseline}`
372
+ : ''
373
+ return `${label} ${seg.value}${base}`
374
+ })
375
+ .join('; ')
376
+
377
+ return (
378
+ <View style={[{ width: '100%' }, style]} accessibilityLabel={defaultAccessibilityLabel}>
379
+ {showLegend ? (
380
+ <View
381
+ style={[
382
+ { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: chartGap },
383
+ legendStyle,
384
+ ]}
385
+ >
386
+ <MetricLegendItem
387
+ label={valueLegendLabel}
388
+ indicatorColor={currentColor}
389
+ modes={modes}
390
+ style={{ flexGrow: 0, flexShrink: 1 }}
391
+ />
392
+ {hasAnyBaseline ? (
393
+ <MetricLegendItem
394
+ label={baselineLegendLabel}
395
+ indicatorColor={baselineColor}
396
+ modes={modes}
397
+ style={{ flexGrow: 0, flexShrink: 1 }}
398
+ />
399
+ ) : null}
400
+ </View>
401
+ ) : null}
402
+
403
+ <View
404
+ accessibilityRole="image"
405
+ style={[
406
+ { flexDirection: 'row', alignItems: 'flex-end', gap: 8, width: '100%' },
407
+ chartStyle,
408
+ ]}
409
+ >
410
+ {data.map((segment, index) => {
411
+ const ratio = Math.max(0, Math.min(1, segment.value / safeMax))
412
+ const barHeightPx = Math.max(0, height * ratio)
413
+ const baselineHeightPx =
414
+ typeof segment.baseline === 'number'
415
+ ? Math.max(0, Math.min(1, segment.baseline / safeMax)) * height
416
+ : null
417
+ const baselineLabel =
418
+ segment.baselineLabel === undefined
419
+ ? typeof segment.baseline === 'number'
420
+ ? formatValue(segment.baseline)
421
+ : undefined
422
+ : segment.baselineLabel
423
+ const resolvedSegment: AllocationSegment = {
424
+ ...segment,
425
+ valueLabel:
426
+ segment.valueLabel === undefined
427
+ ? formatValue(segment.value)
428
+ : segment.valueLabel,
429
+ }
430
+ const showMarker =
431
+ segment.showMarker ?? index === firstBaselineIndex
432
+
433
+ return (
434
+ <SegmentBar
435
+ key={segment.key ?? `segment-${index}`}
436
+ segment={resolvedSegment}
437
+ barHeightPx={barHeightPx}
438
+ baselineHeightPx={baselineHeightPx}
439
+ baselineLabel={baselineLabel}
440
+ showMarker={showMarker}
441
+ theme={theme}
442
+ />
443
+ )
444
+ })}
445
+ </View>
446
+ </View>
447
+ )
448
+ }
449
+
450
+ export default AllocationComparisonChart
@@ -194,20 +194,31 @@ export default function AppBar({
194
194
  justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between',
195
195
  }
196
196
 
197
- // Absolutely-centered middle box for SubPage, mirroring the Figma geometry.
198
- // `left/top: 50%` + a negative translate keeps it centered regardless of the
199
- // bar width, while the fixed width clips overly-wide content (overflow:
200
- // hidden) instead of letting it bleed under the leading/actions slots.
201
- const subPageMiddleStyle: ViewStyle = {
197
+ // Absolutely-positioned overlay for the SubPage middle slot. Instead of
198
+ // centering a fixed-height box with `top/left: 50%` + transform (which on
199
+ // Android resolves the percentage against an intermediate, content-driven
200
+ // parent height and lands a few px too high), the overlay STRETCHES to fill
201
+ // the whole bar (`top/bottom/left/right: 0`) and centers its content with
202
+ // flexbox. This uses the exact same full-height vertical reference as the
203
+ // leading/actions row (`alignItems: 'center'`), so the middle content always
204
+ // sits on the same line as the back arrow and end slot on every platform.
205
+ const subPageMiddleOverlayStyle: ViewStyle = {
202
206
  position: 'absolute',
203
- top: '50%',
204
- left: '50%',
207
+ top: 0,
208
+ left: 0,
209
+ right: 0,
210
+ bottom: 0,
211
+ flexDirection: 'row',
212
+ alignItems: 'center',
213
+ justifyContent: 'center',
214
+ }
215
+
216
+ // Fixed-width clipping box (mirrors the Figma "slot wrap" geometry): keeps the
217
+ // middle content from bleeding under the leading/actions slots while staying
218
+ // centered within the overlay above.
219
+ const subPageMiddleBoxStyle: ViewStyle = {
205
220
  width: middleSlotWidth,
206
221
  height: SUBPAGE_MIDDLE_HEIGHT,
207
- transform: [
208
- { translateX: -middleSlotWidth / 2 },
209
- { translateY: -SUBPAGE_MIDDLE_HEIGHT / 2 },
210
- ],
211
222
  flexDirection: 'row',
212
223
  alignItems: 'center',
213
224
  justifyContent: 'center',
@@ -260,19 +271,21 @@ export default function AppBar({
260
271
  * so its content fills / shrinks within the fixed-width box.
261
272
  */}
262
273
  {isSub && processedMiddle && (
263
- <View style={subPageMiddleStyle} pointerEvents="box-none">
264
- <View
265
- style={{
266
- flex: 1,
267
- minWidth: 1,
268
- height: '100%',
269
- flexDirection: 'row',
270
- alignItems: 'center',
271
- justifyContent: 'center',
272
- }}
273
- pointerEvents="box-none"
274
- >
275
- {processedMiddle}
274
+ <View style={subPageMiddleOverlayStyle} pointerEvents="box-none">
275
+ <View style={subPageMiddleBoxStyle} pointerEvents="box-none">
276
+ <View
277
+ style={{
278
+ flex: 1,
279
+ minWidth: 1,
280
+ height: '100%',
281
+ flexDirection: 'row',
282
+ alignItems: 'center',
283
+ justifyContent: 'center',
284
+ }}
285
+ pointerEvents="box-none"
286
+ >
287
+ {processedMiddle}
288
+ </View>
276
289
  </View>
277
290
  </View>
278
291
  )}