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.
- package/CHANGELOG.md +36 -0
- package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
- package/lib/commonjs/components/AppBar/AppBar.js +36 -22
- package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
- package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
- package/lib/commonjs/components/Attached/Attached.js +34 -4
- package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
- package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
- package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +52 -89
- package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/commonjs/components/index.js +34 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
- package/lib/module/components/AppBar/AppBar.js +36 -22
- package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
- package/lib/module/components/AreaLineChart/chartMath.js +242 -0
- package/lib/module/components/Attached/Attached.js +34 -4
- package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
- package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
- package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
- package/lib/module/components/FullscreenModal/FullscreenModal.js +53 -90
- package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/module/components/index.js +4 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
- package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
- package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
- package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
- package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
- package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +21 -25
- package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
- package/lib/typescript/src/components/index.d.ts +4 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
- package/src/components/AppBar/AppBar.tsx +37 -24
- package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
- package/src/components/AreaLineChart/chartMath.ts +265 -0
- package/src/components/Attached/Attached.tsx +36 -5
- package/src/components/BubbleChart/BubbleChart.tsx +319 -0
- package/src/components/BubbleChart/bubblePacking.ts +397 -0
- package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
- package/src/components/FullscreenModal/FullscreenModal.tsx +61 -119
- package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
- package/src/components/index.ts +4 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- 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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
25
|
-
* `
|
|
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
|
-
|
|
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
|
|
61
|
-
* a scrollable body, a floating close button, and a sticky
|
|
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
|
-
* ###
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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={<
|
|
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,
|
|
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-
|
|
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
|
@@ -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-
|
|
198
|
-
// `left
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
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:
|
|
204
|
-
left:
|
|
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={
|
|
264
|
-
<View
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
)}
|