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
@@ -89,12 +89,13 @@ export default function AppBar({
89
89
  const containerStyle: ViewStyle = {
90
90
  flexDirection: 'row',
91
91
  alignItems: 'center',
92
- justifyContent: 'space-between',
92
+ // No `justifyContent` here: with the inline middle slot using `flex: 1`
93
+ // the three sections lay out naturally (leading | middle | actions).
94
+ // When middleSlot is absent we fall back to `space-between` at the wrapper
95
+ // level so leading & actions still anchor to the edges.
93
96
  paddingHorizontal: paddingHorizontal ?? 16,
94
97
  paddingVertical: paddingVertical ?? (isMain ? 16 : 10),
95
98
  backgroundColor: backgroundColor ?? '#FFFFFF',
96
- // We can set minHeight if we want to enforce consistency, but padding should dictate it mostly.
97
- // Figma shows specific heights implicitly via padding + content.
98
99
  // MainPage: h=68 (16 top/bot padding? 36 height content?)
99
100
  // SubPage: h=52
100
101
  }
@@ -159,9 +160,18 @@ export default function AppBar({
159
160
  ? <View style={actionsStyle}>{cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)}</View>
160
161
  : null
161
162
 
163
+ // When there is no middleSlot we want leading & actions pinned to the
164
+ // outer edges, so we apply `space-between` at the wrapper. With a middle
165
+ // slot present, the middle (flex: 1) absorbs the remaining space, so
166
+ // `space-between` is a no-op.
167
+ const wrapperStyle: ViewStyle = {
168
+ ...containerStyle,
169
+ justifyContent: processedMiddle ? 'flex-start' : 'space-between',
170
+ }
171
+
162
172
  return (
163
173
  <View
164
- style={[containerStyle, style]}
174
+ style={[wrapperStyle, style]}
165
175
  accessibilityRole="header"
166
176
  accessibilityLabel={undefined}
167
177
  {...(accessibilityHint ? { accessibilityHint } : {})}
@@ -172,21 +182,22 @@ export default function AppBar({
172
182
  {processedLeading}
173
183
  </View>
174
184
 
175
- {/* Middle Section (Absolute centered often, or flex? Figma shows "Slot 'Middle'" inside a wrapper that is absolute center) */}
176
- {/* Figma: "absolute flex h-full items-center justify-center left-1/2 ... translate-x-[-50%]" */}
177
- {/* We should only render this wrapper if there IS middle content, to avoid z-index blocking hits. */}
185
+ {/*
186
+ * Middle Section rendered as an in-flow flex item (`flex: 1`) so it
187
+ * occupies the space between leading and actions but never overflows
188
+ * past them. This fixes wide children (e.g. <LinearProgress /> with
189
+ * width: '100%') stretching edge-to-edge under the leading/actions.
190
+ * `minWidth: 0` is required so the flex item can shrink below its
191
+ * content's intrinsic width on platforms that respect it (web).
192
+ */}
178
193
  {processedMiddle && (
179
194
  <View
180
195
  style={{
181
- position: 'absolute',
182
- left: 0,
183
- right: 0,
184
- top: 0,
185
- bottom: 0,
196
+ flex: 1,
197
+ minWidth: 0,
186
198
  alignItems: 'center',
187
199
  justifyContent: 'center',
188
- zIndex: -1, // Behind actions if overlap? Or should be on top?
189
- // Usually middle title shouldn't block actions. `pointerEvents="box-none"` is safer.
200
+ paddingHorizontal: 8,
190
201
  }}
191
202
  pointerEvents="box-none"
192
203
  >
@@ -0,0 +1,235 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ View,
6
+ type ImageSourcePropType,
7
+ type StyleProp,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from 'react-native'
11
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
13
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
14
+ import Avatar from '../Avatar/Avatar'
15
+
16
+ export type BrandChipProps = {
17
+ /** Visible label (e.g. `"Axis Bank • 0245"`). */
18
+ label?: string
19
+ /**
20
+ * Image shown in the avatar. Forwarded to the underlying `Avatar`'s
21
+ * `imageSource` prop. Pass a `require()`-ed asset, an `{ uri }` object
22
+ * or a remote URL string. Ignored when `avatarSlot` is provided.
23
+ */
24
+ imageSource?: ImageSourcePropType | string
25
+ /**
26
+ * Slot replacing the default `Avatar`. Receives `modes` recursively, so
27
+ * inner `Avatar` / `IconCapsule` instances inherit theming.
28
+ */
29
+ avatarSlot?: React.ReactNode
30
+ /** Optional press handler — when provided, the chip becomes a `Pressable`. */
31
+ onPress?: () => void
32
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
33
+ modes?: Record<string, any>
34
+ /** Container style override. */
35
+ style?: StyleProp<ViewStyle>
36
+ /** Label style override. */
37
+ labelStyle?: StyleProp<TextStyle>
38
+ /** Accessibility label. Defaults to `label`. */
39
+ accessibilityLabel?: string
40
+ }
41
+
42
+ interface BrandChipTokens {
43
+ containerStyle: ViewStyle
44
+ labelStyle: TextStyle
45
+ }
46
+
47
+ const toNumber = (value: unknown, fallback: number): number => {
48
+ if (typeof value === 'number') {
49
+ return Number.isFinite(value) ? value : fallback
50
+ }
51
+ if (typeof value === 'string') {
52
+ const parsed = Number(value)
53
+ return Number.isFinite(parsed) ? parsed : fallback
54
+ }
55
+ return fallback
56
+ }
57
+
58
+ const toFontWeight = (
59
+ value: unknown,
60
+ fallback: TextStyle['fontWeight']
61
+ ): TextStyle['fontWeight'] => {
62
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
63
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
64
+ return fallback
65
+ }
66
+
67
+ function resolveBrandChipTokens(modes: Record<string, any>): BrandChipTokens {
68
+ const background =
69
+ (getVariableByName('brandChip/background', modes) as string | null) ??
70
+ '#ffffff'
71
+ const foreground =
72
+ (getVariableByName('brandChip/foreground', modes) as string | null) ??
73
+ '#0d0d0f'
74
+ const borderColor =
75
+ (getVariableByName('brandChip/border/color', modes) as string | null) ??
76
+ '#e0e0e3'
77
+ const borderSize = toNumber(
78
+ getVariableByName('brandChip/border/size', modes),
79
+ 1
80
+ )
81
+ const gap = toNumber(getVariableByName('brandChip/gap', modes), 8)
82
+ const paddingLeft = toNumber(
83
+ getVariableByName('brandChip/padding/left', modes),
84
+ 8
85
+ )
86
+ const paddingRight = toNumber(
87
+ getVariableByName('brandChip/padding/right', modes),
88
+ 12
89
+ )
90
+ const paddingVertical = toNumber(
91
+ getVariableByName('brandChip/padding/vertical', modes),
92
+ 5
93
+ )
94
+ // The Figma token uses 25000 as a sentinel for "always pill-shaped".
95
+ // Cap it to a value React Native renders predictably.
96
+ const radiusRaw = toNumber(getVariableByName('brandChip/radius', modes), 9999)
97
+ const radius = radiusRaw >= 9999 ? 9999 : radiusRaw
98
+
99
+ const fontFamily =
100
+ (getVariableByName('brandChip/label/fontFamily', modes) as string | null) ??
101
+ 'JioType Var'
102
+ const fontSize = toNumber(
103
+ getVariableByName('brandChip/label/fontSize', modes),
104
+ 16
105
+ )
106
+ const lineHeight = toNumber(
107
+ getVariableByName('brandChip/label/lineHeight', modes),
108
+ 21
109
+ )
110
+ const fontWeight = toFontWeight(
111
+ getVariableByName('brandChip/label/fontWeight', modes),
112
+ '500'
113
+ )
114
+
115
+ return {
116
+ containerStyle: {
117
+ flexDirection: 'row',
118
+ alignItems: 'center',
119
+ alignSelf: 'flex-start',
120
+ backgroundColor: background,
121
+ borderColor,
122
+ borderWidth: borderSize,
123
+ borderStyle: 'solid',
124
+ borderRadius: radius,
125
+ paddingLeft,
126
+ paddingRight,
127
+ paddingTop: paddingVertical,
128
+ paddingBottom: paddingVertical,
129
+ gap,
130
+ },
131
+ labelStyle: {
132
+ color: foreground,
133
+ fontFamily,
134
+ fontSize,
135
+ fontWeight,
136
+ lineHeight,
137
+ },
138
+ }
139
+ }
140
+
141
+ // The Figma reference renders the avatar at 29px which is the `S` size in
142
+ // the Avatar Size collection. Callers can override via `modes`.
143
+ const DEFAULT_AVATAR_SIZE_MODE = 'S'
144
+
145
+ /**
146
+ * `BrandChip` renders a compact pill that pairs a small institution avatar
147
+ * with a short identifier label (e.g. `"Axis Bank • 0245"`). It's typically
148
+ * used to surface the currently-selected linked account in headers, sticky
149
+ * bars, or selectors.
150
+ *
151
+ * All visual values resolve through the `brandChip/*` design tokens with
152
+ * sensible Figma defaults so the chip renders correctly out of the box.
153
+ *
154
+ * @component
155
+ * @param {BrandChipProps} props
156
+ */
157
+ function BrandChip({
158
+ label = 'Axis Bank • 0245',
159
+ imageSource,
160
+ avatarSlot,
161
+ onPress,
162
+ modes: propModes = EMPTY_MODES,
163
+ style,
164
+ labelStyle,
165
+ accessibilityLabel,
166
+ }: BrandChipProps) {
167
+ const { modes: globalModes } = useTokens()
168
+ const modes = useMemo(
169
+ () =>
170
+ globalModes === EMPTY_MODES && propModes === EMPTY_MODES
171
+ ? EMPTY_MODES
172
+ : { ...globalModes, ...propModes },
173
+ [globalModes, propModes]
174
+ )
175
+
176
+ const avatarModes = useMemo(
177
+ () => ({ 'Avatar Size': DEFAULT_AVATAR_SIZE_MODE, ...modes }),
178
+ [modes]
179
+ )
180
+
181
+ const tokens = useMemo(() => resolveBrandChipTokens(modes), [modes])
182
+
183
+ const processedAvatarSlot = useMemo(() => {
184
+ if (!avatarSlot) return null
185
+ const processed = cloneChildrenWithModes(avatarSlot, avatarModes)
186
+ return processed.length === 1 ? processed[0] : processed
187
+ }, [avatarSlot, avatarModes])
188
+
189
+ const avatarNode =
190
+ processedAvatarSlot ??
191
+ (imageSource !== undefined ? (
192
+ <Avatar style="Image" imageSource={imageSource} modes={avatarModes} />
193
+ ) : (
194
+ <Avatar style="Image" modes={avatarModes} />
195
+ ))
196
+
197
+ const a11yLabel = accessibilityLabel ?? label
198
+
199
+ const content = (
200
+ <>
201
+ {avatarNode}
202
+ <Text
203
+ style={[tokens.labelStyle, labelStyle]}
204
+ numberOfLines={1}
205
+ ellipsizeMode="tail"
206
+ >
207
+ {label}
208
+ </Text>
209
+ </>
210
+ )
211
+
212
+ if (onPress) {
213
+ return (
214
+ <Pressable
215
+ accessibilityRole="button"
216
+ accessibilityLabel={a11yLabel}
217
+ onPress={onPress}
218
+ style={[tokens.containerStyle, style]}
219
+ >
220
+ {content}
221
+ </Pressable>
222
+ )
223
+ }
224
+
225
+ return (
226
+ <View
227
+ accessibilityLabel={a11yLabel}
228
+ style={[tokens.containerStyle, style]}
229
+ >
230
+ {content}
231
+ </View>
232
+ )
233
+ }
234
+
235
+ export default React.memo(BrandChip)
@@ -0,0 +1,321 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ } from 'react-native'
9
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
+ import Badge from '../Badge/Badge'
12
+ import Button from '../Button/Button'
13
+ import InstitutionBadge from '../InstitutionBadge/InstitutionBadge'
14
+ import type { UnifiedSource } from '../../utils/MediaSource'
15
+
16
+ export type CardBankAccountItem = {
17
+ /** Left-aligned label text (e.g. "Account type"). */
18
+ label: string
19
+ /**
20
+ * Right-aligned value. Strings render through the `text/*` design tokens.
21
+ * Pass any `ReactNode` to fully customise the value (e.g. obfuscated
22
+ * account numbers, masked dots, icons).
23
+ */
24
+ value: React.ReactNode
25
+ }
26
+
27
+ export type CardBankAccountProps = {
28
+ /**
29
+ * Institution name shown in the header next to the avatar. Pass an empty
30
+ * string and `headerSlot` to fully replace the default header.
31
+ */
32
+ institutionName?: string
33
+ /**
34
+ * Avatar source for the institution. Forwarded to the underlying
35
+ * `InstitutionBadge`. Accepts any {@link UnifiedSource}.
36
+ */
37
+ institutionAvatar?: UnifiedSource
38
+ /**
39
+ * Header badge. Pass a string to render the default `Badge` with that
40
+ * label, or a custom `ReactNode` for full control. Pass `false`/`null`
41
+ * to hide the badge entirely.
42
+ */
43
+ badge?: React.ReactNode | string | false | null
44
+ /**
45
+ * Override the entire header. When provided, replaces the default
46
+ * `InstitutionBadge` + `Badge` row.
47
+ */
48
+ headerSlot?: React.ReactNode
49
+ /**
50
+ * Data rows shown in the middle. Each entry renders a left-aligned label
51
+ * and a right-aligned value. Defaults to three placeholder rows that
52
+ * match the Figma reference.
53
+ */
54
+ items?: CardBankAccountItem[]
55
+ /**
56
+ * Custom content slot rendered between the header and the footer.
57
+ * Overrides `items` when provided.
58
+ */
59
+ children?: React.ReactNode
60
+ /** Footer button label. Ignored when `footer` is provided. */
61
+ buttonLabel?: string
62
+ /** Footer button press handler. Ignored when `footer` is provided. */
63
+ onButtonPress?: () => void
64
+ /**
65
+ * Override the footer. Pass a `ReactNode` for full control or
66
+ * `false`/`null` to hide it entirely.
67
+ */
68
+ footer?: React.ReactNode | false | null
69
+ /**
70
+ * Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`).
71
+ *
72
+ * Defaults to `{ 'Button / Size': 'S', AppearanceBrand: 'Secondary',
73
+ * Emphasis: 'Medium' }` so the footer button matches the Figma reference
74
+ * out of the box. Caller-supplied modes are merged on top and can
75
+ * override any of the default keys.
76
+ */
77
+ modes?: Record<string, any>
78
+ /** Container style override. */
79
+ style?: StyleProp<ViewStyle>
80
+ /** Accessibility label for the card region. */
81
+ accessibilityLabel?: string
82
+ }
83
+
84
+ const DEFAULT_ITEMS: CardBankAccountItem[] = [
85
+ { label: 'Account type', value: 'Korem ipsum' },
86
+ { label: 'Account number', value: 'Korem ipsum' },
87
+ { label: 'Last updated', value: 'Korem ipsum' },
88
+ ]
89
+
90
+ // Component-level defaults that match the Figma reference. Caller-provided
91
+ // `modes` are merged on top so every key here can be overridden per-instance.
92
+ const DEFAULT_MODES: Readonly<Record<string, any>> = Object.freeze({
93
+ 'Button / Size': 'S',
94
+ AppearanceBrand: 'Secondary',
95
+ Emphasis: 'Medium',
96
+ })
97
+
98
+ const toNumber = (value: unknown, fallback: number): number => {
99
+ if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
100
+ if (typeof value === 'string') {
101
+ const parsed = Number(value)
102
+ return Number.isFinite(parsed) ? parsed : fallback
103
+ }
104
+ return fallback
105
+ }
106
+
107
+ const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']): TextStyle['fontWeight'] => {
108
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
109
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
110
+ return fallback
111
+ }
112
+
113
+ /**
114
+ * `CardBankAccount` renders a bank account summary card composed of:
115
+ *
116
+ * 1. A header with an `InstitutionBadge` (avatar + bank name) and an
117
+ * optional trailing `Badge`.
118
+ * 2. A configurable list of label/value rows describing the account.
119
+ * 3. A footer `Button` for the primary action.
120
+ *
121
+ * All values resolve through the `bankAccountCard/*` design tokens with
122
+ * sensible Figma defaults so the card renders correctly out of the box.
123
+ *
124
+ * @component
125
+ * @param {CardBankAccountProps} props
126
+ */
127
+ function CardBankAccount({
128
+ institutionName = 'State Bank of India',
129
+ institutionAvatar,
130
+ badge,
131
+ headerSlot,
132
+ items = DEFAULT_ITEMS,
133
+ children,
134
+ buttonLabel = 'Button',
135
+ onButtonPress,
136
+ footer,
137
+ modes: propModes = EMPTY_MODES,
138
+ style,
139
+ accessibilityLabel,
140
+ }: CardBankAccountProps) {
141
+ // Merge caller modes on top of `DEFAULT_MODES` so every default key
142
+ // (e.g. `Button / Size`, `AppearanceBrand`, `Emphasis`) can be overridden
143
+ // per-instance while still applying out of the box.
144
+ const modes = useMemo(
145
+ () =>
146
+ propModes === EMPTY_MODES
147
+ ? DEFAULT_MODES
148
+ : { ...DEFAULT_MODES, ...propModes },
149
+ [propModes],
150
+ )
151
+
152
+ const background =
153
+ (getVariableByName('bankAccountCard/background', modes) as string | null) ?? '#ffffff'
154
+ const radius = toNumber(getVariableByName('bankAccountCard/radius', modes), 16)
155
+ const paddingHorizontal = toNumber(
156
+ getVariableByName('bankAccountCard/padding/horizontal', modes),
157
+ 12
158
+ )
159
+ const paddingVertical = toNumber(
160
+ getVariableByName('bankAccountCard/padding/vertical', modes),
161
+ 16
162
+ )
163
+ const gap = toNumber(getVariableByName('bankAccountCard/gap', modes), 16)
164
+ const headerGap = toNumber(getVariableByName('bankAccountCard/header/gap', modes), 8)
165
+
166
+ const listGroupGap = toNumber(getVariableByName('listGroup/gap', modes), 12)
167
+ const rowGap = toNumber(getVariableByName('listItem/gap', modes), 8)
168
+
169
+ const titleColor =
170
+ (getVariableByName('listItem/title/color', modes) as string | null) ?? '#0f0d0a'
171
+ const titleFontFamily =
172
+ (getVariableByName('listItem/title/fontFamily', modes) as string | null) ?? 'JioType Var'
173
+ const titleFontSize = toNumber(getVariableByName('listItem/title/fontSize', modes), 14)
174
+ const titleLineHeight = toNumber(getVariableByName('listItem/title/lineHeight', modes), 16)
175
+ const titleFontWeight = toFontWeight(
176
+ getVariableByName('listItem/title/fontWeight', modes),
177
+ '700'
178
+ )
179
+
180
+ const valueColor =
181
+ (getVariableByName('text/foreground', modes) as string | null) ?? '#000000'
182
+ const valueFontFamily =
183
+ (getVariableByName('text/fontFamily', modes) as string | null) ?? 'JioType Var'
184
+ const valueFontSize = toNumber(getVariableByName('text/fontSize', modes), 14)
185
+ const valueLineHeight = toNumber(getVariableByName('text/lineHeight', modes), 20)
186
+ const valueLetterSpacing = toNumber(getVariableByName('text/letterSpacing', modes), -0.5)
187
+ const valueFontWeight = toFontWeight(getVariableByName('text/fontWeight', modes), '500')
188
+
189
+ const labelStyle: TextStyle = {
190
+ color: titleColor,
191
+ fontFamily: titleFontFamily,
192
+ fontSize: titleFontSize,
193
+ lineHeight: titleLineHeight,
194
+ fontWeight: titleFontWeight,
195
+ }
196
+
197
+ const valueTextStyle: TextStyle = {
198
+ color: valueColor,
199
+ fontFamily: valueFontFamily,
200
+ fontSize: valueFontSize,
201
+ lineHeight: valueLineHeight,
202
+ letterSpacing: valueLetterSpacing,
203
+ fontWeight: valueFontWeight,
204
+ }
205
+
206
+ const renderBadge = (): React.ReactNode => {
207
+ if (badge === false || badge === null || badge === undefined) return null
208
+ if (typeof badge === 'string') {
209
+ return <Badge label={badge} modes={modes} />
210
+ }
211
+ const processed = cloneChildrenWithModes(badge, modes)
212
+ return processed.length === 1 ? processed[0] : processed
213
+ }
214
+
215
+ const renderHeader = (): React.ReactNode => {
216
+ if (headerSlot !== undefined) {
217
+ const processed = cloneChildrenWithModes(headerSlot, modes)
218
+ return processed.length === 1 ? processed[0] : processed
219
+ }
220
+ return (
221
+ <View
222
+ style={{
223
+ flexDirection: 'row',
224
+ alignItems: 'center',
225
+ gap: headerGap,
226
+ width: '100%',
227
+ }}
228
+ >
229
+ <View style={{ flex: 1, minWidth: 0 }}>
230
+ <InstitutionBadge
231
+ label={institutionName}
232
+ {...(institutionAvatar !== undefined ? { source: institutionAvatar } : {})}
233
+ modes={modes}
234
+ />
235
+ </View>
236
+ {renderBadge()}
237
+ </View>
238
+ )
239
+ }
240
+
241
+ const renderItems = (): React.ReactNode => {
242
+ if (children !== undefined && children !== null) {
243
+ const processed = cloneChildrenWithModes(children, modes)
244
+ if (processed.length === 0) return null
245
+ return (
246
+ <View style={{ width: '100%', gap: listGroupGap }}>
247
+ {processed.length === 1 ? processed[0] : processed}
248
+ </View>
249
+ )
250
+ }
251
+
252
+ if (!items || items.length === 0) return null
253
+
254
+ return (
255
+ <View style={{ width: '100%', gap: listGroupGap }}>
256
+ {items.map((item, index) => (
257
+ <View
258
+ key={`${item.label}-${index}`}
259
+ style={{
260
+ flexDirection: 'row',
261
+ alignItems: 'center',
262
+ gap: rowGap,
263
+ width: '100%',
264
+ }}
265
+ >
266
+ <View style={{ flex: 1, minWidth: 0 }}>
267
+ <Text style={labelStyle}>{item.label}</Text>
268
+ </View>
269
+ {typeof item.value === 'string' || typeof item.value === 'number' ? (
270
+ <Text style={valueTextStyle} numberOfLines={1}>
271
+ {item.value}
272
+ </Text>
273
+ ) : (
274
+ cloneChildrenWithModes(item.value, modes)
275
+ )}
276
+ </View>
277
+ ))}
278
+ </View>
279
+ )
280
+ }
281
+
282
+ const renderFooter = (): React.ReactNode => {
283
+ if (footer === false || footer === null) return null
284
+ if (footer === undefined) {
285
+ return (
286
+ <Button
287
+ label={buttonLabel}
288
+ {...(onButtonPress ? { onPress: onButtonPress } : {})}
289
+ modes={modes}
290
+ style={{ width: '100%' }}
291
+ />
292
+ )
293
+ }
294
+ const processed = cloneChildrenWithModes(footer, modes)
295
+ return processed.length === 1 ? processed[0] : processed
296
+ }
297
+
298
+ return (
299
+ <View
300
+ accessibilityLabel={accessibilityLabel ?? institutionName}
301
+ style={[
302
+ {
303
+ backgroundColor: background,
304
+ borderRadius: radius,
305
+ paddingHorizontal,
306
+ paddingVertical,
307
+ gap,
308
+ alignItems: 'flex-start',
309
+ width: '100%',
310
+ },
311
+ style,
312
+ ]}
313
+ >
314
+ {renderHeader()}
315
+ {renderItems()}
316
+ {renderFooter()}
317
+ </View>
318
+ )
319
+ }
320
+
321
+ export default CardBankAccount