jfs-components 0.0.72 → 0.0.74
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 +28 -0
- package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
- package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
- package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
- package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
- package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
- package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
- package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
- package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
- package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
- package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
- package/lib/commonjs/components/OTP/OTP.js +381 -37
- package/lib/commonjs/components/PageHero/PageHero.js +153 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
- package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
- package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
- package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
- package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
- package/lib/commonjs/components/StatItem/StatItem.js +65 -35
- package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
- package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
- package/lib/commonjs/components/Text/Text.js +9 -2
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +231 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/index.js +7 -0
- package/lib/commonjs/utils/number-utils.js +57 -0
- package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/BrandChip/BrandChip.js +143 -0
- package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
- package/lib/module/components/CardInsight/CardInsight.js +161 -0
- package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
- package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
- package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
- package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
- package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
- package/lib/module/components/DonutChart/DonutChart.js +303 -0
- package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
- package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
- package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
- package/lib/module/components/OTP/OTP.js +381 -38
- package/lib/module/components/PageHero/PageHero.js +147 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
- package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
- package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
- package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
- package/lib/module/components/StatGroup/StatGroup.js +123 -0
- package/lib/module/components/StatItem/StatItem.js +66 -36
- package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
- package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
- package/lib/module/components/Text/Text.js +9 -2
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +28 -2
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/number-utils.js +53 -0
- package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
- package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
- package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
- package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
- package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
- package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
- package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
- package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
- package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
- package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
- package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
- package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
- package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
- package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
- package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
- package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
- package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
- package/lib/typescript/src/components/Text/Text.d.ts +12 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +29 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/index.d.ts +1 -0
- package/lib/typescript/src/utils/number-utils.d.ts +29 -0
- package/package.json +1 -3
- package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/BrandChip/BrandChip.tsx +235 -0
- package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
- package/src/components/CardInsight/CardInsight.tsx +239 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
- package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
- package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
- package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
- package/src/components/CoverageRing/CoverageRing.tsx +225 -0
- package/src/components/DonutChart/DonutChart.tsx +503 -0
- package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/LinearMeter/LinearMeter.tsx +9 -39
- package/src/components/LinearProgress/LinearProgress.tsx +92 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
- package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
- package/src/components/OTP/OTP.tsx +476 -29
- package/src/components/PageHero/PageHero.tsx +200 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/ProductOverview/ProductOverview.tsx +236 -0
- package/src/components/RangeTrack/RangeTrack.tsx +394 -0
- package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
- package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
- package/src/components/StatGroup/StatGroup.tsx +169 -0
- package/src/components/StatItem/StatItem.tsx +117 -40
- package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
- package/src/components/SummaryTile/SummaryTile.tsx +251 -0
- package/src/components/Text/Text.tsx +24 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +47 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/number-utils.ts +60 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
6
|
+
import DonutChart, { type DonutChartSegmentData } from '../DonutChart/DonutChart'
|
|
7
|
+
import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One row of the `DonutChartSummary`. Each item drives BOTH a donut
|
|
11
|
+
* segment and the matching legend row, so the segment and indicator
|
|
12
|
+
* always share the same color by construction.
|
|
13
|
+
*/
|
|
14
|
+
export type DonutChartSummaryItem = {
|
|
15
|
+
/** Stable React key. Falls back to the item label / index. */
|
|
16
|
+
key?: React.Key
|
|
17
|
+
/** The descriptive label rendered next to the indicator dot. */
|
|
18
|
+
label?: React.ReactNode
|
|
19
|
+
/**
|
|
20
|
+
* The data point — drives the segment's angular share. Same
|
|
21
|
+
* semantics as `DonutChartSegmentData.value`. When the legend's
|
|
22
|
+
* right-side text is shown, the resolution order is:
|
|
23
|
+
* `displayValue` → `formatValue(value)` → hide.
|
|
24
|
+
*/
|
|
25
|
+
value: number
|
|
26
|
+
/**
|
|
27
|
+
* Optional override for the legend row's right-side text. Falls
|
|
28
|
+
* back to `formatValue(value)` if a parent-level `formatValue` is
|
|
29
|
+
* provided; otherwise the value slot is hidden (matches the Figma
|
|
30
|
+
* `data` boolean toggle on `MetricLegendItem`). Pass `null` or
|
|
31
|
+
* `false` to explicitly hide on a per-item basis.
|
|
32
|
+
*/
|
|
33
|
+
displayValue?: React.ReactNode
|
|
34
|
+
/**
|
|
35
|
+
* Hard-override the shared color used for both the donut segment
|
|
36
|
+
* and the legend indicator. Bypasses `dataViz/bg` token resolution.
|
|
37
|
+
*/
|
|
38
|
+
color?: string
|
|
39
|
+
/**
|
|
40
|
+
* Per-item design token mode overrides. Merged on top of parent
|
|
41
|
+
* `modes` and the per-index `Appearance / DataViz` defaults
|
|
42
|
+
* (`Senary`, `Primary`, `Secondary`, `Tertiary`, `Quaternary`,
|
|
43
|
+
* `Quinary`, then cycles).
|
|
44
|
+
*/
|
|
45
|
+
modes?: Record<string, any>
|
|
46
|
+
/** Accessibility label for the segment + legend row pairing. */
|
|
47
|
+
accessibilityLabel?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type DonutChartSummaryProps = {
|
|
51
|
+
/**
|
|
52
|
+
* Data-driven rows. Each entry produces a donut segment AND a
|
|
53
|
+
* matching `MetricLegendItem` — the count of legend rows is locked
|
|
54
|
+
* in step with the count of donut segments by construction.
|
|
55
|
+
*/
|
|
56
|
+
items?: DonutChartSummaryItem[]
|
|
57
|
+
/**
|
|
58
|
+
* Optional formatter applied to each item's numeric `value` when
|
|
59
|
+
* the item does not provide an explicit `displayValue`. Useful for
|
|
60
|
+
* the common "format every weight the same way" case
|
|
61
|
+
* (e.g. `(v) => `${v}%``).
|
|
62
|
+
*/
|
|
63
|
+
formatValue?: (value: number) => React.ReactNode
|
|
64
|
+
/** Center value text inside the donut (large/bold). */
|
|
65
|
+
centerValue?: React.ReactNode
|
|
66
|
+
/** Center label text inside the donut (smaller, beneath the value). */
|
|
67
|
+
centerLabel?: React.ReactNode
|
|
68
|
+
/**
|
|
69
|
+
* Slot for fully-custom donut center content. When provided it
|
|
70
|
+
* replaces the default `centerValue`/`centerLabel` text wrap.
|
|
71
|
+
*/
|
|
72
|
+
donutCenter?: React.ReactNode
|
|
73
|
+
/** Outer donut diameter in px. Defaults to 194 (Figma reference). */
|
|
74
|
+
donutSize?: number
|
|
75
|
+
/** Donut ring stroke width in px. Defaults to ~18% of `donutSize`. */
|
|
76
|
+
donutStrokeWidth?: number
|
|
77
|
+
/**
|
|
78
|
+
* Visual gap between adjacent donut segments expressed in degrees
|
|
79
|
+
* of arc. Defaults to 0 (segments touch).
|
|
80
|
+
*/
|
|
81
|
+
donutGap?: number
|
|
82
|
+
/** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
|
|
83
|
+
modes?: Record<string, any>
|
|
84
|
+
/** Override outer container styles. */
|
|
85
|
+
style?: StyleProp<ViewStyle>
|
|
86
|
+
/** Override the legend container styles. */
|
|
87
|
+
legendStyle?: StyleProp<ViewStyle>
|
|
88
|
+
/** Accessibility label announced for the whole summary. */
|
|
89
|
+
accessibilityLabel?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DEFAULT_APPEARANCE_CYCLE = [
|
|
93
|
+
'Senary',
|
|
94
|
+
'Primary',
|
|
95
|
+
'Secondary',
|
|
96
|
+
'Tertiary',
|
|
97
|
+
'Quaternary',
|
|
98
|
+
'Quinary',
|
|
99
|
+
] as const
|
|
100
|
+
|
|
101
|
+
const DEFAULT_ITEMS: DonutChartSummaryItem[] = [
|
|
102
|
+
{ label: 'Equity', value: 40, displayValue: '40%' },
|
|
103
|
+
{ label: 'Recommended coverage', value: 25, displayValue: '25%' },
|
|
104
|
+
{ label: 'Additional benefits', value: 20, displayValue: '20%' },
|
|
105
|
+
{ label: 'Cost analysis', value: 15, displayValue: '15%' },
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const defaultAppearanceFor = (index: number) =>
|
|
109
|
+
DEFAULT_APPEARANCE_CYCLE[index % DEFAULT_APPEARANCE_CYCLE.length]
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the shared color for an item. Honors any explicit `color`
|
|
113
|
+
* override, then falls back to `dataViz/bg` for the merged mode set,
|
|
114
|
+
* then to the Figma reference purple.
|
|
115
|
+
*/
|
|
116
|
+
function resolveItemColor(
|
|
117
|
+
parentModes: Record<string, any>,
|
|
118
|
+
item: DonutChartSummaryItem,
|
|
119
|
+
index: number
|
|
120
|
+
): string {
|
|
121
|
+
if (item.color) return item.color
|
|
122
|
+
const itemModes = {
|
|
123
|
+
...parentModes,
|
|
124
|
+
'Appearance / DataViz': defaultAppearanceFor(index),
|
|
125
|
+
...(item.modes || {}),
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
(getVariableByName('dataViz/bg', itemModes) as string | null) ?? '#5d00b5'
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `DonutChartSummary` pairs a `DonutChart` with a vertical list of
|
|
134
|
+
* `MetricLegendItem` rows. The component takes a single `items` array,
|
|
135
|
+
* so the number of legend rows is locked in step with the number of
|
|
136
|
+
* donut segments by construction — every segment has exactly one
|
|
137
|
+
* legend row, and they share the same color through the same
|
|
138
|
+
* `Appearance / DataViz` cascade as the standalone `DonutChart`.
|
|
139
|
+
*
|
|
140
|
+
* The default 4-item layout receives per-index `Appearance / DataViz`
|
|
141
|
+
* defaults (item 1 → `Senary`, 2 → `Primary`, 3 → `Secondary`, 4 →
|
|
142
|
+
* `Tertiary`, then cycles). Override `modes` per item to remix, or set
|
|
143
|
+
* `Emphasis / DataViz` on the parent `modes` to dim or brighten the
|
|
144
|
+
* whole component at once.
|
|
145
|
+
*
|
|
146
|
+
* @component
|
|
147
|
+
* @param {DonutChartSummaryProps} props
|
|
148
|
+
*/
|
|
149
|
+
function DonutChartSummary({
|
|
150
|
+
items,
|
|
151
|
+
formatValue,
|
|
152
|
+
centerValue = '₹51,230',
|
|
153
|
+
centerLabel = 'Total invested',
|
|
154
|
+
donutCenter,
|
|
155
|
+
donutSize = 194,
|
|
156
|
+
donutStrokeWidth,
|
|
157
|
+
donutGap = 0,
|
|
158
|
+
modes: propModes = EMPTY_MODES,
|
|
159
|
+
style,
|
|
160
|
+
legendStyle,
|
|
161
|
+
accessibilityLabel,
|
|
162
|
+
}: DonutChartSummaryProps) {
|
|
163
|
+
const { modes: globalModes } = useTokens()
|
|
164
|
+
const modes = { ...globalModes, ...propModes }
|
|
165
|
+
|
|
166
|
+
const gap =
|
|
167
|
+
(getVariableByName('donutChartSummary/gap', modes) as number | null) ?? 16
|
|
168
|
+
const legendGap =
|
|
169
|
+
(getVariableByName('donutChartSummary/legend/gap', modes) as
|
|
170
|
+
| number
|
|
171
|
+
| null) ?? 8
|
|
172
|
+
|
|
173
|
+
const resolvedItems = items && items.length > 0 ? items : DEFAULT_ITEMS
|
|
174
|
+
|
|
175
|
+
const segments: DonutChartSegmentData[] = resolvedItems.map(
|
|
176
|
+
(item, index) => ({
|
|
177
|
+
key: item.key ?? `segment-${index}`,
|
|
178
|
+
value: item.value,
|
|
179
|
+
color: resolveItemColor(modes, item, index),
|
|
180
|
+
accessibilityLabel: item.accessibilityLabel,
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const showCustomCenter = donutCenter !== undefined && donutCenter !== null
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<View
|
|
188
|
+
accessibilityRole="summary"
|
|
189
|
+
accessibilityLabel={accessibilityLabel}
|
|
190
|
+
style={[
|
|
191
|
+
{
|
|
192
|
+
width: '100%',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
gap,
|
|
195
|
+
},
|
|
196
|
+
style,
|
|
197
|
+
]}
|
|
198
|
+
>
|
|
199
|
+
<DonutChart
|
|
200
|
+
size={donutSize}
|
|
201
|
+
{...(donutStrokeWidth !== undefined && {
|
|
202
|
+
strokeWidth: donutStrokeWidth,
|
|
203
|
+
})}
|
|
204
|
+
gap={donutGap}
|
|
205
|
+
segments={segments}
|
|
206
|
+
modes={modes}
|
|
207
|
+
{...(!showCustomCenter && {
|
|
208
|
+
value: centerValue,
|
|
209
|
+
label: centerLabel,
|
|
210
|
+
})}
|
|
211
|
+
>
|
|
212
|
+
{showCustomCenter ? donutCenter : null}
|
|
213
|
+
</DonutChart>
|
|
214
|
+
|
|
215
|
+
<View
|
|
216
|
+
style={[
|
|
217
|
+
{
|
|
218
|
+
width: '100%',
|
|
219
|
+
gap: legendGap,
|
|
220
|
+
},
|
|
221
|
+
legendStyle,
|
|
222
|
+
]}
|
|
223
|
+
>
|
|
224
|
+
{resolvedItems.map((item, index) => (
|
|
225
|
+
<MetricLegendItem
|
|
226
|
+
key={item.key ?? `legend-${index}`}
|
|
227
|
+
label={item.label}
|
|
228
|
+
value={resolveLegendValue(item, formatValue)}
|
|
229
|
+
indicatorColor={resolveItemColor(modes, item, index)}
|
|
230
|
+
modes={modes}
|
|
231
|
+
/>
|
|
232
|
+
))}
|
|
233
|
+
</View>
|
|
234
|
+
</View>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve what to render in the legend row's right-side slot. The
|
|
240
|
+
* order of precedence is:
|
|
241
|
+
* 1. `item.displayValue` if explicitly provided (including `null` /
|
|
242
|
+
* `false`, which the underlying `MetricLegendItem` treats as
|
|
243
|
+
* "hide the value slot").
|
|
244
|
+
* 2. `formatValue(item.value)` when a parent-level formatter exists.
|
|
245
|
+
* 3. `undefined` — the value slot is hidden.
|
|
246
|
+
*/
|
|
247
|
+
function resolveLegendValue(
|
|
248
|
+
item: DonutChartSummaryItem,
|
|
249
|
+
formatValue: ((value: number) => React.ReactNode) | undefined
|
|
250
|
+
): React.ReactNode {
|
|
251
|
+
if (item.displayValue !== undefined) return item.displayValue
|
|
252
|
+
if (formatValue) return formatValue(item.value)
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default DonutChartSummary
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type AccessibilityProps,
|
|
9
|
+
type PressableStateCallbackType,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type TextStyle,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from 'react-native'
|
|
14
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
15
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
16
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
17
|
+
import Icon from '../../icons/Icon'
|
|
18
|
+
|
|
19
|
+
const IS_WEB = Platform.OS === 'web'
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// DropdownItem
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export type DropdownItemProps = {
|
|
26
|
+
/** Display text for the item. Ignored when `children` is provided. */
|
|
27
|
+
label?: string
|
|
28
|
+
/**
|
|
29
|
+
* Value associated with the item. Used by the parent `Dropdown` /
|
|
30
|
+
* `DropdownInput` to identify the selected item. Optional when the item
|
|
31
|
+
* is purely informational or used outside of a selection context.
|
|
32
|
+
*/
|
|
33
|
+
value?: string | number | null
|
|
34
|
+
/** Whether the item is currently selected (renders the trailing check). */
|
|
35
|
+
selected?: boolean
|
|
36
|
+
/** Whether the item is disabled and non-interactive. */
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
/** Optional element rendered before the label (icon, avatar, etc.). */
|
|
39
|
+
leading?: React.ReactNode
|
|
40
|
+
/**
|
|
41
|
+
* Optional element rendered after the label. When omitted and `selected`
|
|
42
|
+
* is true, a check icon is rendered automatically.
|
|
43
|
+
*/
|
|
44
|
+
trailing?: React.ReactNode
|
|
45
|
+
/** Press handler. Receives the item's `value` if provided. */
|
|
46
|
+
onPress?: (value: DropdownItemProps['value']) => void
|
|
47
|
+
/** Optional custom child content (overrides `label`). */
|
|
48
|
+
children?: React.ReactNode
|
|
49
|
+
/** Modes for design token resolution. */
|
|
50
|
+
modes?: Record<string, any>
|
|
51
|
+
/** Style overrides for the item container. */
|
|
52
|
+
style?: StyleProp<ViewStyle>
|
|
53
|
+
/** Style overrides for the item label text. */
|
|
54
|
+
labelStyle?: StyleProp<TextStyle>
|
|
55
|
+
/** Accessibility label (defaults to label / inferred from children). */
|
|
56
|
+
accessibilityLabel?: string
|
|
57
|
+
/** Accessibility hint. */
|
|
58
|
+
accessibilityHint?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function useDropdownItemTokens(modes: Record<string, any>) {
|
|
62
|
+
return useMemo(() => {
|
|
63
|
+
// The `dropdownItem/background` token aliases through the
|
|
64
|
+
// `Dropdown Item State` collection (Idle | Selected), so we resolve
|
|
65
|
+
// both possibilities up-front and pick at render time.
|
|
66
|
+
const idleBackground =
|
|
67
|
+
(getVariableByName('dropdownItem/background', {
|
|
68
|
+
...modes,
|
|
69
|
+
'Dropdown Item State': 'Idle',
|
|
70
|
+
}) as string) || '#ffffff'
|
|
71
|
+
const selectedBackground =
|
|
72
|
+
(getVariableByName('dropdownItem/background', {
|
|
73
|
+
...modes,
|
|
74
|
+
'Dropdown Item State': 'Selected',
|
|
75
|
+
}) as string) || '#f5f5f5'
|
|
76
|
+
const foreground =
|
|
77
|
+
(getVariableByName('dropdownItem/foreground', modes) as string) ||
|
|
78
|
+
'#000000'
|
|
79
|
+
const fontFamily =
|
|
80
|
+
(getVariableByName('dropdownItem/fontFamily', modes) as string) ||
|
|
81
|
+
'JioType Var'
|
|
82
|
+
const fontSize =
|
|
83
|
+
parseInt(getVariableByName('dropdownItem/fontSize', modes), 10) || 16
|
|
84
|
+
const fontWeight =
|
|
85
|
+
(getVariableByName('dropdownItem/fontWeight', modes) as string) ||
|
|
86
|
+
'400'
|
|
87
|
+
const lineHeight =
|
|
88
|
+
parseInt(getVariableByName('dropdownItem/lineHeight', modes), 10) || 19
|
|
89
|
+
const gap = parseInt(getVariableByName('dropdownItem/gap', modes), 10) || 8
|
|
90
|
+
const paddingHorizontal =
|
|
91
|
+
parseInt(
|
|
92
|
+
getVariableByName('dropdownItem/padding/horizontal', modes),
|
|
93
|
+
10
|
|
94
|
+
) || 12
|
|
95
|
+
const paddingVertical =
|
|
96
|
+
parseInt(
|
|
97
|
+
getVariableByName('dropdownItem/padding/vertical', modes),
|
|
98
|
+
10
|
|
99
|
+
) || 12
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
idleBackground,
|
|
103
|
+
selectedBackground,
|
|
104
|
+
foreground,
|
|
105
|
+
fontFamily,
|
|
106
|
+
fontSize,
|
|
107
|
+
fontWeight,
|
|
108
|
+
lineHeight,
|
|
109
|
+
gap,
|
|
110
|
+
paddingHorizontal,
|
|
111
|
+
paddingVertical,
|
|
112
|
+
}
|
|
113
|
+
}, [modes])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function DropdownItem({
|
|
117
|
+
label,
|
|
118
|
+
value = null,
|
|
119
|
+
selected = false,
|
|
120
|
+
disabled = false,
|
|
121
|
+
leading,
|
|
122
|
+
trailing,
|
|
123
|
+
onPress,
|
|
124
|
+
children,
|
|
125
|
+
modes: propModes = EMPTY_MODES,
|
|
126
|
+
style,
|
|
127
|
+
labelStyle,
|
|
128
|
+
accessibilityLabel,
|
|
129
|
+
accessibilityHint,
|
|
130
|
+
}: DropdownItemProps) {
|
|
131
|
+
const { modes: globalModes } = useTokens()
|
|
132
|
+
const modes = useMemo(
|
|
133
|
+
() => ({ ...globalModes, ...propModes }),
|
|
134
|
+
[globalModes, propModes]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const tokens = useDropdownItemTokens(modes)
|
|
138
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
139
|
+
|
|
140
|
+
const handlePress = useCallback(() => {
|
|
141
|
+
if (disabled) return
|
|
142
|
+
onPress?.(value)
|
|
143
|
+
}, [disabled, onPress, value])
|
|
144
|
+
|
|
145
|
+
const containerStyle = useCallback(
|
|
146
|
+
({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => {
|
|
147
|
+
const showSelected =
|
|
148
|
+
pressed || (isHovered && IS_WEB) || selected
|
|
149
|
+
const base: ViewStyle = {
|
|
150
|
+
flexDirection: 'row',
|
|
151
|
+
alignItems: 'center',
|
|
152
|
+
gap: tokens.gap,
|
|
153
|
+
paddingHorizontal: tokens.paddingHorizontal,
|
|
154
|
+
paddingVertical: tokens.paddingVertical,
|
|
155
|
+
backgroundColor: showSelected
|
|
156
|
+
? tokens.selectedBackground
|
|
157
|
+
: tokens.idleBackground,
|
|
158
|
+
opacity: disabled ? 0.4 : 1,
|
|
159
|
+
width: '100%',
|
|
160
|
+
}
|
|
161
|
+
return [base, style]
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
tokens.gap,
|
|
165
|
+
tokens.paddingHorizontal,
|
|
166
|
+
tokens.paddingVertical,
|
|
167
|
+
tokens.idleBackground,
|
|
168
|
+
tokens.selectedBackground,
|
|
169
|
+
isHovered,
|
|
170
|
+
selected,
|
|
171
|
+
disabled,
|
|
172
|
+
style,
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const textStyle: TextStyle = {
|
|
177
|
+
color: tokens.foreground,
|
|
178
|
+
fontFamily: tokens.fontFamily,
|
|
179
|
+
fontSize: tokens.fontSize,
|
|
180
|
+
fontWeight: tokens.fontWeight as TextStyle['fontWeight'],
|
|
181
|
+
lineHeight: tokens.lineHeight,
|
|
182
|
+
flexShrink: 1,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const processedLeading = leading
|
|
186
|
+
? cloneChildrenWithModes(React.Children.toArray(leading), modes)
|
|
187
|
+
: null
|
|
188
|
+
|
|
189
|
+
const customTrailing = trailing
|
|
190
|
+
? cloneChildrenWithModes(React.Children.toArray(trailing), modes)
|
|
191
|
+
: null
|
|
192
|
+
|
|
193
|
+
const showDefaultCheck = !trailing && selected
|
|
194
|
+
|
|
195
|
+
const fallbackA11yLabel =
|
|
196
|
+
accessibilityLabel ||
|
|
197
|
+
(typeof label === 'string' ? label : 'Dropdown item')
|
|
198
|
+
|
|
199
|
+
const a11yProps: AccessibilityProps = {
|
|
200
|
+
accessibilityRole: 'menuitem',
|
|
201
|
+
accessibilityLabel: fallbackA11yLabel,
|
|
202
|
+
accessibilityState: { selected, disabled },
|
|
203
|
+
}
|
|
204
|
+
if (accessibilityHint) {
|
|
205
|
+
a11yProps.accessibilityHint = accessibilityHint
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const handleHoverIn = useCallback(() => {
|
|
209
|
+
if (IS_WEB && !disabled) setIsHovered(true)
|
|
210
|
+
}, [disabled])
|
|
211
|
+
const handleHoverOut = useCallback(() => {
|
|
212
|
+
if (IS_WEB) setIsHovered(false)
|
|
213
|
+
}, [])
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Pressable
|
|
217
|
+
onPress={handlePress}
|
|
218
|
+
disabled={disabled}
|
|
219
|
+
onHoverIn={handleHoverIn}
|
|
220
|
+
onHoverOut={handleHoverOut}
|
|
221
|
+
style={containerStyle}
|
|
222
|
+
{...a11yProps}
|
|
223
|
+
>
|
|
224
|
+
{processedLeading}
|
|
225
|
+
{children != null ? (
|
|
226
|
+
children
|
|
227
|
+
) : (
|
|
228
|
+
<Text style={[textStyle, labelStyle]} numberOfLines={1}>
|
|
229
|
+
{label}
|
|
230
|
+
</Text>
|
|
231
|
+
)}
|
|
232
|
+
{customTrailing}
|
|
233
|
+
{showDefaultCheck && (
|
|
234
|
+
<Icon name="ic_confirm" size={16} color={tokens.foreground} />
|
|
235
|
+
)}
|
|
236
|
+
</Pressable>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Dropdown (popup surface)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export type DropdownProps = {
|
|
245
|
+
/** Dropdown items, typically `<DropdownItem />` children. */
|
|
246
|
+
children?: React.ReactNode
|
|
247
|
+
/**
|
|
248
|
+
* Maximum height of the popup before its content becomes scrollable.
|
|
249
|
+
* Defaults to no limit.
|
|
250
|
+
*/
|
|
251
|
+
maxHeight?: number
|
|
252
|
+
/** Modes for design token resolution. */
|
|
253
|
+
modes?: Record<string, any>
|
|
254
|
+
/** Style overrides for the popup container. */
|
|
255
|
+
style?: StyleProp<ViewStyle>
|
|
256
|
+
/** Accessibility label for the menu surface. */
|
|
257
|
+
accessibilityLabel?: string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `Dropdown` is the visual surface (popup) that contains a list of
|
|
262
|
+
* `DropdownItem`s. It is responsible for the background, rounded corners,
|
|
263
|
+
* elevation/shadow, and clipping. Use it standalone for menu UIs, or rely on
|
|
264
|
+
* `DropdownInput` which composes it into a form-field experience.
|
|
265
|
+
*/
|
|
266
|
+
export function Dropdown({
|
|
267
|
+
children,
|
|
268
|
+
maxHeight,
|
|
269
|
+
modes: propModes = EMPTY_MODES,
|
|
270
|
+
style,
|
|
271
|
+
accessibilityLabel,
|
|
272
|
+
}: DropdownProps) {
|
|
273
|
+
const { modes: globalModes } = useTokens()
|
|
274
|
+
const modes = useMemo(
|
|
275
|
+
() => ({ ...globalModes, ...propModes }),
|
|
276
|
+
[globalModes, propModes]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const radius =
|
|
280
|
+
parseInt(getVariableByName('dropdown/radius', modes), 10) || 8
|
|
281
|
+
const background =
|
|
282
|
+
(getVariableByName('dropdown/background', modes) as string) || '#ffffff'
|
|
283
|
+
const shadowColor =
|
|
284
|
+
(getVariableByName('dropdown/shadow/color', modes) as string) ||
|
|
285
|
+
'rgba(0, 0, 0, 0.08)'
|
|
286
|
+
const shadowOffsetX =
|
|
287
|
+
parseInt(getVariableByName('dropdown/shadow/offsetX', modes), 10) || 0
|
|
288
|
+
const shadowOffsetY =
|
|
289
|
+
parseInt(getVariableByName('dropdown/shadow/offsetY', modes), 10) || 4
|
|
290
|
+
const shadowBlur =
|
|
291
|
+
parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16
|
|
292
|
+
|
|
293
|
+
const containerStyle: ViewStyle = {
|
|
294
|
+
backgroundColor: background,
|
|
295
|
+
borderRadius: radius,
|
|
296
|
+
overflow: 'hidden',
|
|
297
|
+
shadowColor,
|
|
298
|
+
shadowOffset: { width: shadowOffsetX, height: shadowOffsetY },
|
|
299
|
+
shadowOpacity: 1,
|
|
300
|
+
shadowRadius: shadowBlur / 2,
|
|
301
|
+
elevation: 4,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const content = (
|
|
305
|
+
<View style={{ flexDirection: 'column' }}>
|
|
306
|
+
{cloneChildrenWithModes(children, modes)}
|
|
307
|
+
</View>
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<View
|
|
312
|
+
style={[containerStyle, style]}
|
|
313
|
+
accessibilityRole="menu"
|
|
314
|
+
accessibilityLabel={accessibilityLabel || 'Dropdown menu'}
|
|
315
|
+
>
|
|
316
|
+
{maxHeight != null ? (
|
|
317
|
+
<ScrollView
|
|
318
|
+
style={{ maxHeight }}
|
|
319
|
+
showsVerticalScrollIndicator={true}
|
|
320
|
+
keyboardShouldPersistTaps="handled"
|
|
321
|
+
>
|
|
322
|
+
{content}
|
|
323
|
+
</ScrollView>
|
|
324
|
+
) : (
|
|
325
|
+
content
|
|
326
|
+
)}
|
|
327
|
+
</View>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export default Dropdown
|