jfs-components 0.0.85 → 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 +7 -0
- package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +52 -89
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
- package/lib/module/components/FullscreenModal/FullscreenModal.js +53 -90
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +21 -25
- package/lib/typescript/src/components/index.d.ts +1 -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/FullscreenModal/FullscreenModal.tsx +61 -119
- package/src/components/index.ts +1 -0
- package/src/icons/registry.ts +1 -1
package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* One vertical pill in the {@link AllocationComparisonChartProps.data} array.
|
|
5
|
+
*
|
|
6
|
+
* Each segment renders a single bar whose **height encodes `value`** (the
|
|
7
|
+
* "current" reading) and, when supplied, a **`baseline`** overlay drawn from
|
|
8
|
+
* the bottom up with a dashed marker line (the "recommended" reading). Both
|
|
9
|
+
* are measured against the same shared scale so bars and baselines are
|
|
10
|
+
* directly comparable across segments.
|
|
11
|
+
*/
|
|
12
|
+
export type AllocationSegment = {
|
|
13
|
+
/** Stable React key (falls back to the array index). */
|
|
14
|
+
key?: React.Key;
|
|
15
|
+
/** Caption rendered under the bar — e.g. `"Small & Mid"`. */
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Primary value driving the bar's height (the "current" reading). Scaled
|
|
19
|
+
* against {@link AllocationComparisonChartProps.max}.
|
|
20
|
+
*/
|
|
21
|
+
value: number;
|
|
22
|
+
/**
|
|
23
|
+
* Optional comparison value (the "recommended" reading). When provided, a
|
|
24
|
+
* filled overlay is drawn from the bottom of the bar up to this level.
|
|
25
|
+
* Omit to render a plain bar.
|
|
26
|
+
*/
|
|
27
|
+
baseline?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to draw the dashed reference line + value label at the top of the
|
|
30
|
+
* `baseline` overlay. Defaults to `true` **only for the first segment that
|
|
31
|
+
* has a `baseline`** and `false` for the rest, so the marker stays a
|
|
32
|
+
* focused callout rather than repeating on every bar. Set explicitly to
|
|
33
|
+
* force it on/off per segment. Has no effect without a `baseline`.
|
|
34
|
+
*/
|
|
35
|
+
showMarker?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Text shown above the bar. Defaults to `formatValue(value)`. Pass `null`
|
|
38
|
+
* to hide it.
|
|
39
|
+
*/
|
|
40
|
+
valueLabel?: React.ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Text shown beside the dashed baseline marker. Defaults to
|
|
43
|
+
* `formatValue(baseline)`. Pass `null` to hide just the marker label while
|
|
44
|
+
* keeping the dashed line.
|
|
45
|
+
*/
|
|
46
|
+
baselineLabel?: React.ReactNode;
|
|
47
|
+
/** Hard-override the bar (current) fill color. */
|
|
48
|
+
color?: string;
|
|
49
|
+
/** Hard-override the baseline (recommended) overlay color. */
|
|
50
|
+
baselineColor?: string;
|
|
51
|
+
/** Per-segment accessibility label. */
|
|
52
|
+
accessibilityLabel?: string;
|
|
53
|
+
};
|
|
54
|
+
export type AllocationComparisonChartProps = {
|
|
55
|
+
/**
|
|
56
|
+
* The segments to plot, left → right. Each carries its `value`, optional
|
|
57
|
+
* `baseline` and its `label`, so a bar can never drift from its caption.
|
|
58
|
+
*/
|
|
59
|
+
data?: AllocationSegment[];
|
|
60
|
+
/**
|
|
61
|
+
* Maximum value used to scale every bar **and** baseline. Defaults to the
|
|
62
|
+
* largest `value`/`baseline` across `data`. Pass an explicit `max` (e.g.
|
|
63
|
+
* `100` for percentages) for a fixed scale.
|
|
64
|
+
*/
|
|
65
|
+
max?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Height in px of the bar area — i.e. the height of a bar whose value
|
|
68
|
+
* equals `max`. Excludes the value/caption label rows. Default: `154`.
|
|
69
|
+
*/
|
|
70
|
+
height?: number;
|
|
71
|
+
/** Width of each pill bar in px. Default: the `segmentIndicator/track/width` token (`28`). */
|
|
72
|
+
barWidth?: number;
|
|
73
|
+
/** Show the legend row above the chart. Default: `true`. */
|
|
74
|
+
showLegend?: boolean;
|
|
75
|
+
/** Legend label for the bar (current) series. Default: `"Current"`. */
|
|
76
|
+
valueLegendLabel?: React.ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* Legend label for the baseline (recommended) series. Default:
|
|
79
|
+
* `"Recommended"`. The baseline legend item only appears when at least one
|
|
80
|
+
* segment defines a `baseline`.
|
|
81
|
+
*/
|
|
82
|
+
baselineLegendLabel?: React.ReactNode;
|
|
83
|
+
/**
|
|
84
|
+
* Formats numeric `value`/`baseline` into the default labels. Default:
|
|
85
|
+
* `(v) => \`${v}%\``.
|
|
86
|
+
*/
|
|
87
|
+
formatValue?: (value: number) => string;
|
|
88
|
+
/** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
|
|
89
|
+
modes?: Record<string, any>;
|
|
90
|
+
/** Container style override. */
|
|
91
|
+
style?: StyleProp<ViewStyle>;
|
|
92
|
+
/** Style applied to the bars row. */
|
|
93
|
+
chartStyle?: StyleProp<ViewStyle>;
|
|
94
|
+
/** Style applied to the legend row. */
|
|
95
|
+
legendStyle?: StyleProp<ViewStyle>;
|
|
96
|
+
/** Accessibility label for the whole chart. */
|
|
97
|
+
accessibilityLabel?: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* `AllocationComparisonChart` plots a row of vertical pill bars that compare a
|
|
101
|
+
* **current** reading (each bar's height) against an optional **recommended**
|
|
102
|
+
* baseline (a filled overlay drawn from the bottom up, marked with a dashed
|
|
103
|
+
* line). Every bar and baseline shares a single scale, so heights are directly
|
|
104
|
+
* comparable across segments — no axes required.
|
|
105
|
+
*
|
|
106
|
+
* The chart is driven entirely by the `data` array: each entry pairs a
|
|
107
|
+
* `value`, an optional `baseline` and its `label`, so a bar can never drift
|
|
108
|
+
* out of sync with its caption or its baseline marker.
|
|
109
|
+
*
|
|
110
|
+
* Colors, fonts, spacing and the pill radius resolve from the Figma
|
|
111
|
+
* `segmentIndicator/*`, `metricLegendItem/*` and `allocationComparisonChart/*`
|
|
112
|
+
* tokens via the `modes` prop.
|
|
113
|
+
*
|
|
114
|
+
* @component
|
|
115
|
+
*/
|
|
116
|
+
declare function AllocationComparisonChart({ data, max, height, barWidth, showLegend, valueLegendLabel, baselineLegendLabel, formatValue, modes: propModes, style, chartStyle, legendStyle, accessibilityLabel, }: AllocationComparisonChartProps): import("react/jsx-runtime").JSX.Element;
|
|
117
|
+
export default AllocationComparisonChart;
|
|
118
|
+
//# sourceMappingURL=AllocationComparisonChart.d.ts.map
|
|
@@ -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
|
|
@@ -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';
|
|
@@ -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
|