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.
Files changed (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  6. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
  7. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  8. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
  10. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  11. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  12. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  13. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  14. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  15. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  16. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  17. package/lib/commonjs/components/FormField/FormField.js +328 -178
  18. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  19. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  20. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  21. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  22. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  23. package/lib/commonjs/components/OTP/OTP.js +381 -37
  24. package/lib/commonjs/components/PageHero/PageHero.js +153 -0
  25. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  26. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  27. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  28. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  29. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  30. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  31. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  32. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  33. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  34. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  35. package/lib/commonjs/components/Text/Text.js +9 -2
  36. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  37. package/lib/commonjs/components/index.js +231 -1
  38. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/commonjs/icons/registry.js +1 -1
  40. package/lib/commonjs/utils/index.js +7 -0
  41. package/lib/commonjs/utils/number-utils.js +57 -0
  42. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  43. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  44. package/lib/module/components/AppBar/AppBar.js +17 -11
  45. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  46. package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
  47. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  48. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  49. package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
  50. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  51. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  52. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  53. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  54. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  55. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  56. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  57. package/lib/module/components/FormField/FormField.js +330 -180
  58. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  59. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  60. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  61. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  62. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  63. package/lib/module/components/OTP/OTP.js +381 -38
  64. package/lib/module/components/PageHero/PageHero.js +147 -0
  65. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  66. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  67. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  68. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  69. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  70. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  71. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  72. package/lib/module/components/StatItem/StatItem.js +66 -36
  73. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  74. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  75. package/lib/module/components/Text/Text.js +9 -2
  76. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  77. package/lib/module/components/index.js +28 -2
  78. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  79. package/lib/module/icons/registry.js +1 -1
  80. package/lib/module/utils/index.js +2 -1
  81. package/lib/module/utils/number-utils.js +53 -0
  82. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  83. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  84. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  85. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
  86. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  87. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  88. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
  89. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  90. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  91. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  92. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  93. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  94. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  95. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  96. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  97. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  98. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  99. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  100. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  101. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  102. package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
  103. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  104. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  105. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  106. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  107. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  108. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  109. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  110. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  111. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  112. package/lib/typescript/src/components/Text/Text.d.ts +12 -2
  113. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  114. package/lib/typescript/src/components/index.d.ts +29 -3
  115. package/lib/typescript/src/icons/registry.d.ts +1 -1
  116. package/lib/typescript/src/utils/index.d.ts +1 -0
  117. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  118. package/package.json +1 -3
  119. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  120. package/src/components/AccountCard/AccountCard.tsx +376 -0
  121. package/src/components/AppBar/AppBar.tsx +25 -14
  122. package/src/components/BrandChip/BrandChip.tsx +235 -0
  123. package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
  124. package/src/components/CardInsight/CardInsight.tsx +239 -0
  125. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  126. package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
  127. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  128. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  129. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  130. package/src/components/DonutChart/DonutChart.tsx +503 -0
  131. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  132. package/src/components/Dropdown/Dropdown.tsx +331 -0
  133. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  134. package/src/components/FormField/FormField.tsx +542 -215
  135. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  136. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  137. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  138. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  139. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  140. package/src/components/OTP/OTP.tsx +476 -29
  141. package/src/components/PageHero/PageHero.tsx +200 -0
  142. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  143. package/src/components/PoweredByLabel/finvu.png +0 -0
  144. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  145. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  146. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  147. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  148. package/src/components/StatGroup/StatGroup.tsx +169 -0
  149. package/src/components/StatItem/StatItem.tsx +117 -40
  150. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  151. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  152. package/src/components/Text/Text.tsx +24 -3
  153. package/src/components/Tooltip/Tooltip.tsx +50 -25
  154. package/src/components/index.ts +47 -3
  155. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  156. package/src/icons/registry.ts +1 -1
  157. package/src/utils/index.ts +1 -0
  158. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,268 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ type StyleProp,
5
+ type ViewStyle,
6
+ } from 'react-native'
7
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
8
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
9
+ import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils'
10
+
11
+ /**
12
+ * Per-segment data definition for the data-driven `segments` prop.
13
+ *
14
+ * Use `value` for proportional widths (segments share width by their value
15
+ * relative to the sum). When `value` is omitted, segments share the row
16
+ * equally (`flex: 1`).
17
+ *
18
+ * Use `modes` to override the per-segment design-token mode — typically the
19
+ * `Appearance / DataViz` mode for color theming, or `Emphasis / DataViz` to
20
+ * change the emphasis level. The parent already injects per-index defaults
21
+ * (segment 0 = High, 1 = Medium, 2 = Low, then cycling) so callers only need
22
+ * to pass `modes` when they want a different result.
23
+ */
24
+ export type SegmentedTrackSegmentData = {
25
+ /** Stable React key. */
26
+ key?: React.Key
27
+ /**
28
+ * Optional explicit weight used for proportional width (`flex: value`).
29
+ * When omitted, the segment uses an equal share of the available row.
30
+ * Mixing values and undefined is supported — undefined falls back to 1.
31
+ */
32
+ value?: number
33
+ /**
34
+ * Hard-override the segment color. Bypasses `dataViz/bg` token resolution
35
+ * entirely.
36
+ */
37
+ color?: string
38
+ /**
39
+ * Per-segment design token mode overrides. Merged on top of the parent
40
+ * `modes` and the per-index Emphasis defaults.
41
+ */
42
+ modes?: Record<string, any>
43
+ /** Override individual segment styles. */
44
+ style?: StyleProp<ViewStyle>
45
+ /** Per-segment accessibility label. */
46
+ accessibilityLabel?: string
47
+ }
48
+
49
+ export type SegmentedTrackProps = {
50
+ /**
51
+ * Data-driven segment list. Used when no `children` slot is provided.
52
+ * Defaults to three equal segments which receive per-index Emphasis
53
+ * defaults (`High`, `Medium`, `Low`).
54
+ */
55
+ segments?: SegmentedTrackSegmentData[]
56
+ /**
57
+ * Slot for fully custom segment children. Each top-level child is treated
58
+ * as one segment. The parent injects per-index Emphasis defaults and the
59
+ * shared `modes`, while child-level `modes` overrides win.
60
+ */
61
+ children?: React.ReactNode
62
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
63
+ modes?: Record<string, any>
64
+ /** Override the container styles. */
65
+ style?: StyleProp<ViewStyle>
66
+ /** Style applied to every segment (data-driven mode only). */
67
+ segmentStyle?: StyleProp<ViewStyle>
68
+ /** Accessibility label announced for the whole distribution row. */
69
+ accessibilityLabel?: string
70
+ }
71
+
72
+ type SegmentedTrackSegmentProps = {
73
+ /**
74
+ * Optional explicit weight used for proportional width (`flex: value`).
75
+ * Defaults to 1 (equal share within the parent row).
76
+ */
77
+ value?: number | undefined
78
+ /**
79
+ * Hard-override the segment color. Bypasses `dataViz/bg` token resolution.
80
+ */
81
+ color?: string | undefined
82
+ /**
83
+ * Design token modes for the segment. Merged with parent `modes` and the
84
+ * per-index Emphasis defaults injected by the parent `SegmentedTrack`.
85
+ */
86
+ modes?: Record<string, any> | undefined
87
+ /** Override the segment styles. */
88
+ style?: StyleProp<ViewStyle> | undefined
89
+ /** Per-segment accessibility label. */
90
+ accessibilityLabel?: string | undefined
91
+ }
92
+
93
+ /**
94
+ * Default per-index Emphasis modes applied to every segment when the caller
95
+ * does not provide its own `Emphasis / DataViz` override. Cycles for >3
96
+ * segments so additional segments fall back to the same High/Medium/Low
97
+ * rotation.
98
+ */
99
+ const DEFAULT_EMPHASIS_CYCLE = ['High', 'Medium', 'Low'] as const
100
+
101
+ const DEFAULT_SEGMENTS: SegmentedTrackSegmentData[] = [
102
+ {},
103
+ {},
104
+ {},
105
+ ]
106
+
107
+ /**
108
+ * Compute the default `Emphasis / DataViz` mode for a segment at `index`.
109
+ * Cycles through {@link DEFAULT_EMPHASIS_CYCLE} so any number of segments
110
+ * gets a sensible default.
111
+ */
112
+ function defaultEmphasisFor(index: number) {
113
+ return DEFAULT_EMPHASIS_CYCLE[index % DEFAULT_EMPHASIS_CYCLE.length]
114
+ }
115
+
116
+ function SegmentedTrackSegment({
117
+ value = 1,
118
+ color,
119
+ modes = EMPTY_MODES,
120
+ style,
121
+ accessibilityLabel,
122
+ }: SegmentedTrackSegmentProps) {
123
+ const resolvedColor =
124
+ color ?? ((getVariableByName('dataViz/bg', modes) as string | null) ?? '#5d00b5')
125
+
126
+ return (
127
+ <View
128
+ accessibilityLabel={accessibilityLabel}
129
+ style={[
130
+ {
131
+ flex: value,
132
+ minWidth: 1,
133
+ height: '100%',
134
+ backgroundColor: resolvedColor,
135
+ },
136
+ style,
137
+ ]}
138
+ />
139
+ )
140
+ }
141
+
142
+ /**
143
+ * `SegmentedTrack` renders a horizontal pill-shaped row of categorical
144
+ * segments. Use it for distribution / share-of breakdowns where each
145
+ * segment is a sibling category (not a directional or temporal progress).
146
+ *
147
+ * Defaults to three equal segments tinted at descending Emphasis levels
148
+ * (`High`, `Medium`, `Low`) so the shape reads as a single concept split
149
+ * three ways. Consumers can either pass the data-driven `segments` prop or
150
+ * a fully custom `children` slot of `SegmentedTrack.Segment`s.
151
+ *
152
+ * Each segment resolves its color through the `dataViz/bg` token, which
153
+ * cascades through `Emphasis / DataViz` and `Appearance / DataViz`. Pass
154
+ * `modes` per segment (or override `Appearance / DataViz` at the parent
155
+ * level via `modes`) to retheme the row without touching colors directly.
156
+ *
157
+ * @component
158
+ * @param {SegmentedTrackProps} props
159
+ */
160
+ function SegmentedTrack({
161
+ segments,
162
+ children,
163
+ modes: propModes = EMPTY_MODES,
164
+ style,
165
+ segmentStyle,
166
+ accessibilityLabel,
167
+ }: SegmentedTrackProps) {
168
+ const { modes: globalModes } = useTokens()
169
+ const modes = { ...globalModes, ...propModes }
170
+
171
+ const trackHeight =
172
+ (getVariableByName('segmentedTrack/height', modes) as number | null) ?? 24
173
+ const trackRadius =
174
+ (getVariableByName('segmentedTrack/radius', modes) as number | null) ?? 999
175
+
176
+ const renderedSegments = renderSegments({
177
+ segments,
178
+ children,
179
+ modes,
180
+ segmentStyle,
181
+ })
182
+
183
+ return (
184
+ <View
185
+ accessibilityRole="image"
186
+ accessibilityLabel={accessibilityLabel}
187
+ style={[
188
+ {
189
+ flexDirection: 'row',
190
+ alignItems: 'stretch',
191
+ height: trackHeight,
192
+ borderRadius: trackRadius,
193
+ overflow: 'hidden',
194
+ width: '100%',
195
+ backgroundColor: 'transparent',
196
+ },
197
+ style,
198
+ ]}
199
+ >
200
+ {renderedSegments}
201
+ </View>
202
+ )
203
+ }
204
+
205
+ /**
206
+ * Build the slot children. When the caller passes JSX `children`, every
207
+ * top-level element is treated as one segment and receives merged `modes`
208
+ * (parent + per-index Emphasis default + the child's own `modes` taking
209
+ * priority). Otherwise the data-driven `segments` array is rendered.
210
+ */
211
+ function renderSegments({
212
+ segments,
213
+ children,
214
+ modes,
215
+ segmentStyle,
216
+ }: {
217
+ segments: SegmentedTrackSegmentData[] | undefined
218
+ children: React.ReactNode | undefined
219
+ modes: Record<string, any>
220
+ segmentStyle: StyleProp<ViewStyle> | undefined
221
+ }): React.ReactNode[] {
222
+ if (children !== undefined && children !== null) {
223
+ const flat = flattenChildren(children)
224
+ return flat.map((child, index) => {
225
+ if (!React.isValidElement(child)) {
226
+ return child
227
+ }
228
+ const childProps = (child.props as any) ?? {}
229
+ const childModes = childProps.modes
230
+ const mergedModes = {
231
+ ...modes,
232
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
233
+ ...(childModes || {}),
234
+ }
235
+ return React.cloneElement(child, {
236
+ ...childProps,
237
+ modes: mergedModes,
238
+ key: child.key ?? `segment-${index}`,
239
+ })
240
+ })
241
+ }
242
+
243
+ const list = segments && segments.length > 0 ? segments : DEFAULT_SEGMENTS
244
+ return list.map((segment, index) => {
245
+ const segmentModes = {
246
+ ...modes,
247
+ 'Emphasis / DataViz': defaultEmphasisFor(index),
248
+ ...(segment.modes || {}),
249
+ }
250
+ return (
251
+ <SegmentedTrackSegment
252
+ key={segment.key ?? `segment-${index}`}
253
+ value={segment.value ?? 1}
254
+ color={segment.color}
255
+ modes={segmentModes}
256
+ style={[segmentStyle, segment.style]}
257
+ accessibilityLabel={segment.accessibilityLabel}
258
+ />
259
+ )
260
+ })
261
+ }
262
+
263
+ SegmentedTrack.Segment = SegmentedTrackSegment
264
+
265
+ export { SegmentedTrackSegment }
266
+ export type { SegmentedTrackSegmentProps }
267
+
268
+ export default SegmentedTrack
@@ -0,0 +1,169 @@
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 {
5
+ EMPTY_MODES,
6
+ cloneChildrenWithModes,
7
+ flattenChildren,
8
+ } from '../../utils/react-utils'
9
+ import StatItem from '../StatItem/StatItem'
10
+ import Divider from '../Divider/Divider'
11
+
12
+ export type StatGroupItem = {
13
+ /** Stable key for the item. Falls back to the label / index. */
14
+ key?: React.Key
15
+ /** The descriptive label shown beneath the value. */
16
+ label?: string
17
+ /** The prominent value shown above the label. */
18
+ value?: string
19
+ }
20
+
21
+ export type StatGroupProps = {
22
+ /**
23
+ * Array of stat items. Each item is rendered as a centered `StatItem`
24
+ * (`labelPosition="Bottom"`) with vertical dividers between them.
25
+ *
26
+ * Ignored when a `children` slot is provided.
27
+ */
28
+ items?: StatGroupItem[]
29
+ /**
30
+ * Custom slot content. When provided, the `items` prop is ignored. Each
31
+ * top-level child is wrapped to take an equal share of the available width
32
+ * and a vertical `Divider` is auto-inserted between siblings — matching the
33
+ * Figma `slot` behaviour. `modes` are propagated to all children.
34
+ */
35
+ children?: React.ReactNode
36
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
37
+ modes?: Record<string, any>
38
+ /** Override container styles. */
39
+ style?: StyleProp<ViewStyle>
40
+ }
41
+
42
+ const DEFAULT_ITEMS: StatGroupItem[] = [
43
+ { label: 'Updates', value: 'Daily' },
44
+ { label: 'Data range', value: '24 months' },
45
+ { label: 'Validity', value: '24 months' },
46
+ { label: 'Stored for', value: '1 month' },
47
+ ]
48
+
49
+ /**
50
+ * StatGroup renders a card-style container holding a horizontal row of
51
+ * `StatItem`s separated by vertical dividers. It is typically used to surface
52
+ * 3–5 short metrics (e.g. validity, data range, storage) at a glance.
53
+ *
54
+ * Pass `items` for the simple data-driven case, or use the `children` slot
55
+ * for full control over the row contents (the component still auto-inserts
56
+ * vertical dividers between top-level children).
57
+ *
58
+ * @component
59
+ * @param {StatGroupProps} props
60
+ */
61
+ function StatGroup({
62
+ items,
63
+ children,
64
+ modes = EMPTY_MODES,
65
+ style,
66
+ }: StatGroupProps) {
67
+ const background =
68
+ (getVariableByName('statGroup/background', modes) as string | null) ?? '#ffffff'
69
+ const strokeColor =
70
+ (getVariableByName('statGroup/stroke/color', modes) as string | null) ?? '#ebebed'
71
+ const strokeSize =
72
+ (getVariableByName('statGroup/stroke/size', modes) as number | null) ?? 1
73
+ const radius =
74
+ (getVariableByName('statGroup/radius', modes) as number | null) ?? 12
75
+ const paddingTop =
76
+ (getVariableByName('statGroup/padding/top', modes) as number | null) ?? 16
77
+ const paddingRight =
78
+ (getVariableByName('statGroup/padding/right', modes) as number | null) ?? 0
79
+ const paddingBottom =
80
+ (getVariableByName('statGroup/padding/bottom', modes) as number | null) ?? 12
81
+ const paddingLeft =
82
+ (getVariableByName('statGroup/padding/left', modes) as number | null) ?? 0
83
+
84
+ const containerStyle: ViewStyle = {
85
+ backgroundColor: background,
86
+ borderColor: strokeColor,
87
+ borderWidth: strokeSize,
88
+ borderStyle: 'solid',
89
+ borderRadius: radius,
90
+ paddingTop,
91
+ paddingRight,
92
+ paddingBottom,
93
+ paddingLeft,
94
+ overflow: 'hidden',
95
+ }
96
+
97
+ const slotChildren = renderSlotChildren({ items, children, modes })
98
+
99
+ return (
100
+ <View style={[containerStyle, style]} accessibilityRole="summary">
101
+ <View
102
+ style={{
103
+ flexDirection: 'row',
104
+ alignItems: 'stretch',
105
+ width: '100%',
106
+ }}
107
+ >
108
+ {slotChildren}
109
+ </View>
110
+ </View>
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Build the row of items: render either the supplied children or the
116
+ * `items` array, wrap each entry so it grows equally, and inject a vertical
117
+ * `Divider` between siblings.
118
+ */
119
+ function renderSlotChildren({
120
+ items,
121
+ children,
122
+ modes,
123
+ }: {
124
+ items?: StatGroupItem[]
125
+ children?: React.ReactNode
126
+ modes: Record<string, any>
127
+ }): React.ReactNode[] {
128
+ let nodes: React.ReactNode[]
129
+
130
+ if (children !== undefined && children !== null) {
131
+ const cloned = cloneChildrenWithModes(children, modes)
132
+ nodes = flattenChildren(cloned)
133
+ } else {
134
+ const list = items && items.length > 0 ? items : DEFAULT_ITEMS
135
+ nodes = list.map((item, index) => (
136
+ <StatItem
137
+ key={item.key ?? `${item.label ?? 'item'}-${index}`}
138
+ label={item.label}
139
+ value={item.value}
140
+ labelPosition="Bottom"
141
+ modes={modes}
142
+ />
143
+ ))
144
+ }
145
+
146
+ const result: React.ReactNode[] = []
147
+ nodes.forEach((node, index) => {
148
+ if (index > 0) {
149
+ result.push(
150
+ <Divider
151
+ key={`divider-${index}`}
152
+ direction="vertical"
153
+ modes={modes}
154
+ />
155
+ )
156
+ }
157
+ result.push(
158
+ <View
159
+ key={`slot-${index}`}
160
+ style={{ flex: 1, minWidth: 0, alignSelf: 'center' }}
161
+ >
162
+ {node}
163
+ </View>
164
+ )
165
+ })
166
+ return result
167
+ }
168
+
169
+ export default StatGroup
@@ -1,22 +1,46 @@
1
1
  import React from 'react'
2
- import { View, Text, type StyleProp, type ViewStyle } from 'react-native'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ } from 'react-native'
3
9
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
10
  import { EMPTY_MODES } from '../../utils/react-utils'
5
11
 
12
+ export type StatItemLabelPosition = 'Top' | 'Bottom'
13
+
6
14
  export type StatItemProps = {
7
- /** The small descriptive label above the value. */
15
+ /** The small descriptive label. */
8
16
  label?: string
9
- /** The large prominent value to display. */
17
+ /** The prominent value to display. */
10
18
  value?: string
11
- /** Design token modes for theming. */
19
+ /**
20
+ * Whether the label is rendered above or below the value. Matches the
21
+ * Figma `labelPosition` variant. Defaults to `'Top'`.
22
+ *
23
+ * - `'Top'` — Label (12px medium) above large value (26px black). Left-aligned.
24
+ * - `'Bottom'` — Value (12px medium) above smaller label (10px regular). Center-aligned.
25
+ */
26
+ labelPosition?: StatItemLabelPosition
27
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
12
28
  modes?: Record<string, any>
13
29
  /** Override container styles. */
14
30
  style?: StyleProp<ViewStyle>
31
+ /** Override the label text styles. */
32
+ labelStyle?: StyleProp<TextStyle>
33
+ /** Override the value text styles. */
34
+ valueStyle?: StyleProp<TextStyle>
15
35
  }
16
36
 
17
37
  /**
18
- * StatItem displays a label/value pair with the value in a large, bold style.
19
- * Useful for metrics, product stats, or KPI callouts.
38
+ * StatItem displays a label/value pair, useful for product stats, metrics
39
+ * or KPI callouts.
40
+ *
41
+ * Supports two layouts via the `labelPosition` variant:
42
+ * - `'Top'` — Small label above a large prominent value (default).
43
+ * - `'Bottom'` — Value above a smaller label, content centered.
20
44
  *
21
45
  * @component
22
46
  * @param {StatItemProps} props
@@ -24,47 +48,100 @@ export type StatItemProps = {
24
48
  function StatItem({
25
49
  label = 'Purity verified by NABL',
26
50
  value = '99.99%',
51
+ labelPosition = 'Top',
27
52
  modes = EMPTY_MODES,
28
53
  style,
54
+ labelStyle,
55
+ valueStyle,
29
56
  }: StatItemProps) {
30
- const gap = getVariableByName('statItem/gap', modes) ?? 2
57
+ const isBottom = labelPosition === 'Bottom'
58
+
59
+ // The Figma `Bottom` variant overrides token values locally inside the
60
+ // component (it is not exposed in the design-tokens JSON), so the resolved
61
+ // token values reflect only the `Top` variant. For the `Bottom` variant we
62
+ // therefore use hardcoded literals that match the Figma design, while still
63
+ // allowing the resolver to surface any future mode-based overrides for the
64
+ // `Top` variant.
65
+ const gap = isBottom
66
+ ? 6
67
+ : ((getVariableByName('statItem/gap', modes) as number | null) ?? 2)
68
+
69
+ const labelForeground = isBottom
70
+ ? '#24262b'
71
+ : ((getVariableByName('statItem/label/foreground', modes) as string | null) ?? '#1a1c1f')
72
+ const labelFontFamily =
73
+ (getVariableByName('statItem/label/fontFamily', modes) as string | null) ?? 'JioType Var'
74
+ const labelFontSize = isBottom
75
+ ? 10
76
+ : ((getVariableByName('statItem/label/fontSize', modes) as number | null) ?? 12)
77
+ const labelFontWeightRaw = isBottom
78
+ ? 400
79
+ : (getVariableByName('statItem/label/fontWeight', modes) ?? 500)
80
+ const labelFontWeight = String(labelFontWeightRaw) as TextStyle['fontWeight']
81
+ const labelLineHeight = isBottom
82
+ ? 13
83
+ : ((getVariableByName('statItem/label/lineHeight', modes) as number | null) ?? 16)
84
+
85
+ const valueForeground = isBottom
86
+ ? '#141414'
87
+ : ((getVariableByName('statItem/value/foreground', modes) as string | null) ?? '#0d0d0f')
88
+ const valueFontFamily =
89
+ (getVariableByName('statItem/value/fontFamily', modes) as string | null) ?? 'JioType Var'
90
+ const valueFontSize = isBottom
91
+ ? 12
92
+ : ((getVariableByName('statItem/value/fontSize', modes) as number | null) ?? 26)
93
+ const valueFontWeightRaw = isBottom
94
+ ? 500
95
+ : (getVariableByName('statItem/value/fontWeight', modes) ?? 900)
96
+ const valueFontWeight = String(valueFontWeightRaw) as TextStyle['fontWeight']
97
+ const valueLineHeight = isBottom
98
+ ? 16
99
+ : ((getVariableByName('statItem/value/lineHeight', modes) as number | null) ?? 26)
31
100
 
32
- const labelForeground = getVariableByName('statItem/label/foreground', modes) ?? '#1a1c1f'
33
- const labelFontFamily = getVariableByName('statItem/label/fontFamily', modes) ?? 'JioType Var'
34
- const labelFontSize = getVariableByName('statItem/label/fontSize', modes) ?? 12
35
- const labelFontWeight = getVariableByName('statItem/label/fontWeight', modes) ?? '500'
36
- const labelLineHeight = getVariableByName('statItem/label/lineHeight', modes) ?? 16
101
+ const containerStyle: ViewStyle = {
102
+ gap,
103
+ alignItems: isBottom ? 'center' : 'flex-start',
104
+ justifyContent: isBottom ? 'center' : 'flex-start',
105
+ }
37
106
 
38
- const valueForeground = getVariableByName('statItem/value/foreground', modes) ?? '#0d0d0f'
39
- const valueFontFamily = getVariableByName('statItem/value/fontFamily', modes) ?? 'JioType Var'
40
- const valueFontSize = getVariableByName('statItem/value/fontSize', modes) ?? 26
41
- const valueFontWeight = getVariableByName('statItem/value/fontWeight', modes) ?? '900'
42
- const valueLineHeight = getVariableByName('statItem/value/lineHeight', modes) ?? 26
107
+ const labelTextStyle: TextStyle = {
108
+ color: labelForeground,
109
+ fontFamily: labelFontFamily,
110
+ fontSize: labelFontSize,
111
+ fontWeight: labelFontWeight,
112
+ lineHeight: labelLineHeight,
113
+ textAlign: isBottom ? 'center' : 'left',
114
+ }
115
+
116
+ const valueTextStyle: TextStyle = {
117
+ color: valueForeground,
118
+ fontFamily: valueFontFamily,
119
+ fontSize: valueFontSize,
120
+ fontWeight: valueFontWeight,
121
+ lineHeight: valueLineHeight,
122
+ textAlign: isBottom ? 'center' : 'left',
123
+ }
124
+
125
+ const labelNode = (
126
+ <Text style={[labelTextStyle, labelStyle]}>{label}</Text>
127
+ )
128
+ const valueNode = (
129
+ <Text style={[valueTextStyle, valueStyle]}>{value}</Text>
130
+ )
43
131
 
44
132
  return (
45
- <View style={[{ gap: gap as number }, style]}>
46
- <Text
47
- style={{
48
- color: labelForeground as string,
49
- fontFamily: labelFontFamily as string,
50
- fontSize: labelFontSize as number,
51
- fontWeight: String(labelFontWeight) as any,
52
- lineHeight: labelLineHeight as number,
53
- }}
54
- >
55
- {label}
56
- </Text>
57
- <Text
58
- style={{
59
- color: valueForeground as string,
60
- fontFamily: valueFontFamily as string,
61
- fontSize: valueFontSize as number,
62
- fontWeight: String(valueFontWeight) as any,
63
- lineHeight: valueLineHeight as number,
64
- }}
65
- >
66
- {value}
67
- </Text>
133
+ <View style={[containerStyle, style]}>
134
+ {isBottom ? (
135
+ <>
136
+ {valueNode}
137
+ {labelNode}
138
+ </>
139
+ ) : (
140
+ <>
141
+ {labelNode}
142
+ {valueNode}
143
+ </>
144
+ )}
68
145
  </View>
69
146
  )
70
147
  }