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,323 @@
1
+ import React, { useCallback, useState } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ LayoutAnimation,
7
+ Platform,
8
+ UIManager,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ type TextStyle,
12
+ } from 'react-native'
13
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
14
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
15
+ import Checkbox from '../Checkbox/Checkbox'
16
+ import Divider from '../Divider/Divider'
17
+ import Icon from '../../icons/Icon'
18
+
19
+ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
20
+ UIManager.setLayoutAnimationEnabledExperimental(true)
21
+ }
22
+
23
+ export type AccordionCheckboxProps = {
24
+ /** Title shown next to the checkbox (e.g. "Axis Bank"). */
25
+ title?: string
26
+ /** Subtitle shown beneath the title (e.g. "3 accounts"). */
27
+ subtitle?: string
28
+ /** Initial expanded state (uncontrolled). Defaults to `false`. */
29
+ defaultExpanded?: boolean
30
+ /** Controlled expanded state. When provided, the component becomes controlled. */
31
+ expanded?: boolean
32
+ /** Callback fired whenever the expanded state changes. */
33
+ onExpandedChange?: (expanded: boolean) => void
34
+ /** Initial checked state of the header checkbox (uncontrolled). */
35
+ defaultChecked?: boolean
36
+ /** Controlled checked state of the header checkbox. */
37
+ checked?: boolean
38
+ /** Callback fired whenever the header checkbox value changes. */
39
+ onCheckedChange?: (checked: boolean) => void
40
+ /** Disables the entire component (header press + checkbox + content). */
41
+ disabled?: boolean
42
+ /**
43
+ * Slot — content rendered below the divider when expanded. Typically a
44
+ * `CheckboxGroup` of child `CheckboxItem`s. The `modes` prop is forwarded
45
+ * recursively to every child.
46
+ */
47
+ children?: React.ReactNode
48
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
49
+ modes?: Record<string, any>
50
+ /** Override outer container styles. */
51
+ style?: StyleProp<ViewStyle>
52
+ /** Accessibility label for the header press target. Defaults to `title`. */
53
+ accessibilityLabel?: string
54
+ }
55
+
56
+ /**
57
+ * AccordionCheckbox composes a header (with a `Checkbox`, title, subtitle and
58
+ * chevron) and a collapsible content slot. It mirrors the Figma
59
+ * "Accordion Checkbox" component.
60
+ *
61
+ * Two independent interactions live inside the header:
62
+ * - **Pressing the checkbox** toggles the parent checked state via
63
+ * `onCheckedChange` (typically used for "select all" behaviour).
64
+ * - **Pressing anywhere else on the header** toggles the expanded state via
65
+ * `onExpandedChange`.
66
+ *
67
+ * When expanded, a divider is shown and `children` are rendered with `modes`
68
+ * automatically forwarded through `cloneChildrenWithModes`.
69
+ *
70
+ * @component
71
+ * @param {AccordionCheckboxProps} props
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * <AccordionCheckbox
76
+ * title="Axis Bank"
77
+ * subtitle="3 accounts"
78
+ * defaultExpanded
79
+ * defaultChecked
80
+ * modes={{ 'Color Mode': 'Light' }}
81
+ * >
82
+ * <CheckboxGroup>
83
+ * <CheckboxItem label="Fixed deposit • 0245" />
84
+ * <CheckboxItem label="Recurring deposit • 1182" />
85
+ * <CheckboxItem label="Mutual fund • Equity" />
86
+ * </CheckboxGroup>
87
+ * </AccordionCheckbox>
88
+ * ```
89
+ */
90
+ function AccordionCheckbox({
91
+ title = 'Axis Bank',
92
+ subtitle = '3 accounts',
93
+ defaultExpanded = false,
94
+ expanded: controlledExpanded,
95
+ onExpandedChange,
96
+ defaultChecked = false,
97
+ checked: controlledChecked,
98
+ onCheckedChange,
99
+ disabled = false,
100
+ children,
101
+ modes = EMPTY_MODES,
102
+ style,
103
+ accessibilityLabel,
104
+ }: AccordionCheckboxProps) {
105
+ const isExpandedControlled = controlledExpanded !== undefined
106
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded)
107
+ const isExpanded = isExpandedControlled ? controlledExpanded : internalExpanded
108
+
109
+ const isCheckedControlled = controlledChecked !== undefined
110
+ const [internalChecked, setInternalChecked] = useState(defaultChecked)
111
+ const isChecked = isCheckedControlled ? controlledChecked : internalChecked
112
+
113
+ const handleToggleExpanded = useCallback(() => {
114
+ if (disabled) return
115
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
116
+ const next = !isExpanded
117
+ if (!isExpandedControlled) {
118
+ setInternalExpanded(next)
119
+ }
120
+ onExpandedChange?.(next)
121
+ }, [disabled, isExpanded, isExpandedControlled, onExpandedChange])
122
+
123
+ const handleToggleChecked = useCallback(
124
+ (next: boolean) => {
125
+ if (disabled) return
126
+ if (!isCheckedControlled) {
127
+ setInternalChecked(next)
128
+ }
129
+ onCheckedChange?.(next)
130
+ },
131
+ [disabled, isCheckedControlled, onCheckedChange]
132
+ )
133
+
134
+ const background =
135
+ (getVariableByName('accordionCheckbox/background', modes) as string | null) ?? '#ffffff'
136
+ const radius = (getVariableByName('accordionCheckbox/radius', modes) as number | null) ?? 12
137
+ const paddingHorizontal =
138
+ (getVariableByName('accordionCheckbox/padding/horizontal', modes) as number | null) ?? 12
139
+ const paddingVertical =
140
+ (getVariableByName('accordionCheckbox/padding/vertical', modes) as number | null) ?? 16
141
+ const containerGap =
142
+ (getVariableByName('accordionCheckbox/gap', modes) as number | null) ?? 12
143
+ const borderColor =
144
+ (getVariableByName('accordionCheckbox/border/color', modes) as string | null) ?? '#ebebed'
145
+ const borderSize =
146
+ (getVariableByName('accordionCheckbox/border/size', modes) as number | null) ?? 1
147
+
148
+ const headerGap =
149
+ (getVariableByName('accordionCheckbox/header/gap', modes) as number | null) ?? 12
150
+ const headerPaddingHorizontal =
151
+ (getVariableByName('accordionCheckbox/header/padding/horizontal', modes) as number | null) ?? 0
152
+ const headerPaddingVertical =
153
+ (getVariableByName('accordionCheckbox/header/padding/vertical', modes) as number | null) ?? 0
154
+ const headerWrapGap =
155
+ (getVariableByName('accordionCheckbox/header/wrap/gap', modes) as number | null) ?? 12
156
+
157
+ const textWrapGap =
158
+ (getVariableByName('accordionCheckbox/textWrap/gap', modes) as number | null) ?? 2
159
+
160
+ const titleColor =
161
+ (getVariableByName('accordionCheckbox/title/color', modes) as string | null) ?? '#000000'
162
+ const titleFontSize =
163
+ (getVariableByName('accordionCheckbox/title/fontSize', modes) as number | null) ?? 14
164
+ const titleFontFamily =
165
+ (getVariableByName('accordionCheckbox/title/fontFamily', modes) as string | null) ?? 'JioType Var'
166
+ const titleLineHeight =
167
+ (getVariableByName('accordionCheckbox/title/lineHeight', modes) as number | null) ?? 18
168
+ const titleFontWeightRaw =
169
+ getVariableByName('accordionCheckbox/title/fontWeight', modes) ?? 600
170
+ const titleFontWeight = String(titleFontWeightRaw) as TextStyle['fontWeight']
171
+
172
+ const subtitleColor =
173
+ (getVariableByName('accordionCheckbox/subtitle/color', modes) as string | null) ?? '#23190a'
174
+ const subtitleFontSize =
175
+ (getVariableByName('accordionCheckbox/subtitle/fontSize', modes) as number | null) ?? 12
176
+ const subtitleFontFamily =
177
+ (getVariableByName('accordionCheckbox/subtitle/fontFamily', modes) as string | null) ?? 'JioType Var'
178
+ const subtitleLineHeight =
179
+ (getVariableByName('accordionCheckbox/subtitle/lineHeight', modes) as number | null) ?? 16
180
+ const subtitleFontWeightRaw =
181
+ getVariableByName('accordionCheckbox/subtitle/fontWeight', modes) ?? 500
182
+ const subtitleFontWeight = String(subtitleFontWeightRaw) as TextStyle['fontWeight']
183
+
184
+ const iconColor =
185
+ (getVariableByName('accordionCheckbox/icon/color', modes) as string | null) ?? '#000000'
186
+ const iconSize =
187
+ (getVariableByName('accordionCheckbox/icon/size', modes) as number | null) ?? 24
188
+
189
+ const contentGap =
190
+ (getVariableByName('accordionCheckbox/content/gap', modes) as number | null) ?? 0
191
+ const contentPaddingTop =
192
+ (getVariableByName('accordionCheckbox/content/padding/top', modes) as number | null) ?? 8
193
+ const contentPaddingBottom =
194
+ (getVariableByName('accordionCheckbox/content/padding/bottom', modes) as number | null) ?? 0
195
+ const contentPaddingHorizontal =
196
+ (getVariableByName('accordionCheckbox/content/padding/horizontal', modes) as number | null) ?? 0
197
+
198
+ const containerStyle: ViewStyle = {
199
+ backgroundColor: background,
200
+ borderRadius: radius,
201
+ paddingHorizontal,
202
+ paddingVertical,
203
+ flexDirection: 'column',
204
+ alignItems: 'stretch',
205
+ gap: containerGap,
206
+ overflow: 'hidden',
207
+ ...(isExpanded
208
+ ? { borderWidth: borderSize, borderColor }
209
+ : null),
210
+ ...(disabled ? { opacity: 0.6 } : null),
211
+ }
212
+
213
+ const headerStyle: ViewStyle = {
214
+ flexDirection: 'row',
215
+ alignItems: 'center',
216
+ gap: headerGap,
217
+ paddingHorizontal: headerPaddingHorizontal,
218
+ paddingVertical: headerPaddingVertical,
219
+ }
220
+
221
+ const wrapStyle: ViewStyle = {
222
+ flex: 1,
223
+ minWidth: 0,
224
+ flexDirection: 'row',
225
+ alignItems: 'center',
226
+ gap: headerWrapGap,
227
+ }
228
+
229
+ const textWrapStyle: ViewStyle = {
230
+ flex: 1,
231
+ minWidth: 0,
232
+ flexDirection: 'column',
233
+ alignItems: 'flex-start',
234
+ gap: textWrapGap,
235
+ }
236
+
237
+ const titleStyle: TextStyle = {
238
+ color: titleColor,
239
+ fontSize: titleFontSize,
240
+ lineHeight: titleLineHeight,
241
+ fontFamily: titleFontFamily,
242
+ fontWeight: titleFontWeight,
243
+ }
244
+
245
+ const subtitleStyle: TextStyle = {
246
+ color: subtitleColor,
247
+ fontSize: subtitleFontSize,
248
+ lineHeight: subtitleLineHeight,
249
+ fontFamily: subtitleFontFamily,
250
+ fontWeight: subtitleFontWeight,
251
+ }
252
+
253
+ const contentStyle: ViewStyle = {
254
+ flexDirection: 'column',
255
+ alignItems: 'stretch',
256
+ justifyContent: 'center',
257
+ gap: contentGap,
258
+ paddingTop: contentPaddingTop,
259
+ paddingBottom: contentPaddingBottom,
260
+ paddingHorizontal: contentPaddingHorizontal,
261
+ }
262
+
263
+ const a11yLabel = accessibilityLabel ?? title
264
+
265
+ return (
266
+ <View style={[containerStyle, style]}>
267
+ <Pressable
268
+ onPress={handleToggleExpanded}
269
+ disabled={disabled}
270
+ accessibilityRole="button"
271
+ accessibilityState={{ expanded: isExpanded, disabled }}
272
+ accessibilityLabel={a11yLabel}
273
+ accessibilityHint={isExpanded ? 'Collapse' : 'Expand'}
274
+ style={({ pressed }) => [
275
+ headerStyle,
276
+ pressed && !disabled ? { opacity: 0.9 } : null,
277
+ ]}
278
+ >
279
+ <View style={wrapStyle}>
280
+ <Checkbox
281
+ checked={isChecked}
282
+ onValueChange={handleToggleChecked}
283
+ disabled={disabled}
284
+ modes={modes}
285
+ accessibilityLabel={
286
+ typeof title === 'string' ? `Select ${title}` : undefined
287
+ }
288
+ />
289
+ <View style={textWrapStyle}>
290
+ <Text style={titleStyle} numberOfLines={1} selectable={false}>
291
+ {title}
292
+ </Text>
293
+ {subtitle ? (
294
+ <Text style={subtitleStyle} numberOfLines={1} selectable={false}>
295
+ {subtitle}
296
+ </Text>
297
+ ) : null}
298
+ </View>
299
+ </View>
300
+ <Icon
301
+ name={isExpanded ? 'ic_chevron_up' : 'ic_chevron_down'}
302
+ size={iconSize}
303
+ color={iconColor}
304
+ accessibilityElementsHidden={true}
305
+ importantForAccessibility="no"
306
+ />
307
+ </Pressable>
308
+
309
+ {isExpanded ? (
310
+ <>
311
+ <Divider modes={modes} />
312
+ {children ? (
313
+ <View style={contentStyle}>
314
+ {cloneChildrenWithModes(children, modes)}
315
+ </View>
316
+ ) : null}
317
+ </>
318
+ ) : null}
319
+ </View>
320
+ )
321
+ }
322
+
323
+ export default AccordionCheckbox
@@ -0,0 +1,376 @@
1
+ import React, { useCallback, useMemo, useRef } from 'react'
2
+ import {
3
+ Image as RNImage,
4
+ Platform,
5
+ Pressable,
6
+ Text,
7
+ View,
8
+ type ImageSourcePropType,
9
+ type ImageStyle,
10
+ type PressableStateCallbackType,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle,
14
+ } from 'react-native'
15
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
16
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
17
+ import Icon from '../../icons/Icon'
18
+
19
+ const IS_IOS = Platform.OS === 'ios'
20
+ const PRESS_DELAY = IS_IOS ? 130 : 0
21
+ const DEFAULT_IMAGE_RATIO = 125 / 82
22
+
23
+ const pressedOverlayStyle: ViewStyle = { opacity: 0.7 }
24
+
25
+ // Default modes for the "Add" placeholder icon (`iconButton/*` tokens).
26
+ // Caller-supplied `modes` are merged on top so every key here can be overridden.
27
+ const ADD_ICON_DEFAULT_MODES: Readonly<Record<string, any>> = Object.freeze({
28
+ AppearanceBrand: 'Secondary',
29
+ Emphasis: 'Low',
30
+ "Button / Size": "M",
31
+ })
32
+
33
+ export type AccountCardState = 'connected' | 'add'
34
+
35
+ export type AccountCardProps = {
36
+ /**
37
+ * Visual variant of the card.
38
+ * - `'connected'` (default): renders a card preview image, a `title` and a
39
+ * `subtitle` below it (e.g. account name + masked number).
40
+ * - `'add'`: renders an empty placeholder field with a centred `+` icon
41
+ * and only the `title`. Use this for the "Add new account" affordance
42
+ * that pairs with a list of connected `AccountCard`s.
43
+ */
44
+ state?: AccountCardState
45
+ /** Title rendered below the image / placeholder. */
46
+ title?: string
47
+ /**
48
+ * Subtitle rendered below the title (e.g. masked account number).
49
+ * Only displayed when `state === 'connected'`.
50
+ */
51
+ subtitle?: string
52
+ /**
53
+ * Card preview image. Only used when `state === 'connected'` and
54
+ * `cardSlot` is not provided. Accepts a URL string or any
55
+ * `ImageSourcePropType` (e.g. `require('./card.png')`).
56
+ */
57
+ imageSource?: ImageSourcePropType | string
58
+ /**
59
+ * Aspect ratio (width / height) of the image / placeholder area.
60
+ * Defaults to the Figma reference ratio (`125 / 82` ≈ `1.524`).
61
+ */
62
+ imageRatio?: number
63
+ /**
64
+ * Override the entire image / placeholder area with a custom node
65
+ * (e.g. a smaller `DebitCard`, a brand logo, a remote image).
66
+ * Receives `modes` automatically through `cloneChildrenWithModes`.
67
+ */
68
+ cardSlot?: React.ReactNode
69
+ /**
70
+ * Icon name shown inside the "Add" placeholder. Defaults to `'ic_add'`.
71
+ * Only used when `state === 'add'`.
72
+ */
73
+ addIcon?: string
74
+ /**
75
+ * Press handler. Applied to the whole card so the entire surface is a
76
+ * single touch target — particularly important for the `'add'` state,
77
+ * which behaves like a button.
78
+ */
79
+ onPress?: () => void
80
+ /** Disable interaction (also dims the card). */
81
+ disabled?: boolean
82
+ /** Design token modes (e.g. `{ 'Color Mode': 'Light' }`). */
83
+ modes?: Record<string, any>
84
+ /** Container style override. */
85
+ style?: StyleProp<ViewStyle>
86
+ /** Accessibility label (defaults to `title`). */
87
+ accessibilityLabel?: string
88
+ /** Accessibility hint forwarded to the underlying `Pressable`. */
89
+ accessibilityHint?: string
90
+ }
91
+
92
+ const toNumber = (value: unknown, fallback: number): number => {
93
+ if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
94
+ if (typeof value === 'string') {
95
+ const parsed = Number(value)
96
+ return Number.isFinite(parsed) ? parsed : fallback
97
+ }
98
+ return fallback
99
+ }
100
+
101
+ const toFontWeight = (
102
+ value: unknown,
103
+ fallback: TextStyle['fontWeight']
104
+ ): TextStyle['fontWeight'] => {
105
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
106
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
107
+ return fallback
108
+ }
109
+
110
+ const normalizeImageSource = (
111
+ src: AccountCardProps['imageSource']
112
+ ): ImageSourcePropType | undefined => {
113
+ if (src == null) return undefined
114
+ if (typeof src === 'string') return { uri: src }
115
+ return src
116
+ }
117
+
118
+ /**
119
+ * `AccountCard` — a compact card preview used to represent a linked
120
+ * financial account in lists / grids.
121
+ *
122
+ * Two visual states are supported via the `state` prop:
123
+ *
124
+ * 1. `'connected'` (default): renders a small card-art preview, a bold
125
+ * `title` and a regular-weight `subtitle` (e.g. masked account number).
126
+ * 2. `'add'`: renders a soft-tinted placeholder field with a centred `+`
127
+ * icon and the `title` underneath, intended as the "add new account"
128
+ * entry-point at the end of a list of connected accounts.
129
+ *
130
+ * All values resolve through the `accountCard/*` design tokens with
131
+ * sensible Figma defaults so the card renders correctly out of the box.
132
+ *
133
+ * @component
134
+ * @param {AccountCardProps} props
135
+ */
136
+ function AccountCard({
137
+ state = 'connected',
138
+ title = 'Personal account',
139
+ subtitle = '**** 5651',
140
+ imageSource,
141
+ imageRatio = DEFAULT_IMAGE_RATIO,
142
+ cardSlot,
143
+ addIcon = 'ic_add',
144
+ onPress,
145
+ disabled = false,
146
+ modes = EMPTY_MODES,
147
+ style,
148
+ accessibilityLabel,
149
+ accessibilityHint,
150
+ }: AccountCardProps) {
151
+ const iconModes = useMemo(
152
+ () =>
153
+ modes === EMPTY_MODES
154
+ ? ADD_ICON_DEFAULT_MODES
155
+ : { ...ADD_ICON_DEFAULT_MODES, ...modes },
156
+ [modes],
157
+ )
158
+
159
+ // ---- Tokens ---------------------------------------------------------
160
+ const gap = toNumber(getVariableByName('accountCard/gap', modes), 8)
161
+ const textWrapGap = toNumber(getVariableByName('accountCard/textWrap/gap', modes), 0)
162
+
163
+ const titleColor =
164
+ (getVariableByName('accountCard/title/foreground', modes) as string | null) ?? '#0d0d0f'
165
+ const titleFontFamily =
166
+ (getVariableByName('accountCard/title/fontFamily', modes) as string | null) ?? 'JioType Var'
167
+ const titleFontSize = toNumber(getVariableByName('accountCard/title/fontSize', modes), 12)
168
+ const titleLineHeight = toNumber(getVariableByName('accountCard/title/lineHeight', modes), 16)
169
+ const titleFontWeight = toFontWeight(
170
+ getVariableByName('accountCard/title/fontWeight', modes),
171
+ '700'
172
+ )
173
+
174
+ const subtitleColor =
175
+ (getVariableByName('accountCard/subtitle/foreground', modes) as string | null) ?? '#24262b'
176
+ const subtitleFontFamily =
177
+ (getVariableByName('accountCard/subtitle/fontFamily', modes) as string | null) ??
178
+ 'JioType Var'
179
+ const subtitleFontSize = toNumber(
180
+ getVariableByName('accountCard/subtitle/fontSize', modes),
181
+ 12
182
+ )
183
+ const subtitleLineHeight = toNumber(
184
+ getVariableByName('accountCard/subtitle/lineHeight', modes),
185
+ 16
186
+ )
187
+ const subtitleFontWeight = toFontWeight(
188
+ getVariableByName('accountCard/subtitle/fontWeight', modes),
189
+ '400'
190
+ )
191
+
192
+ const addFieldRadius = toNumber(
193
+ getVariableByName('accountCard/addItemField/radius', modes),
194
+ 8
195
+ )
196
+ const addFieldBg =
197
+ (getVariableByName('accountCard/addItemField/bg', modes) as string | null) ?? '#ede8ff'
198
+
199
+ const addIconColor =
200
+ (getVariableByName('iconButton/icon/color', iconModes) as string | null) ?? '#5d00b5'
201
+ const addIconSize = toNumber(getVariableByName('iconButton/icon/size', iconModes), 16)
202
+
203
+ // ---- Styles ---------------------------------------------------------
204
+ const titleStyle: TextStyle = {
205
+ color: titleColor,
206
+ fontFamily: titleFontFamily,
207
+ fontSize: titleFontSize,
208
+ lineHeight: titleLineHeight,
209
+ fontWeight: titleFontWeight,
210
+ width: '100%',
211
+ }
212
+
213
+ const subtitleStyle: TextStyle = {
214
+ color: subtitleColor,
215
+ fontFamily: subtitleFontFamily,
216
+ fontSize: subtitleFontSize,
217
+ lineHeight: subtitleLineHeight,
218
+ fontWeight: subtitleFontWeight,
219
+ width: '100%',
220
+ }
221
+
222
+ const imageBoxStyle: ViewStyle = {
223
+ width: '100%',
224
+ aspectRatio: imageRatio,
225
+ overflow: 'hidden',
226
+ }
227
+
228
+ // RN's `<Image>` accepts `ImageStyle`, which has a narrower `overflow`
229
+ // union than `ViewStyle`. Build the image-only style separately so the
230
+ // shared box dimensions can be reused without a cast.
231
+ const imageStyle: ImageStyle = {
232
+ width: '100%',
233
+ aspectRatio: imageRatio,
234
+ }
235
+
236
+ const addFieldStyle: ViewStyle = {
237
+ ...imageBoxStyle,
238
+ backgroundColor: addFieldBg,
239
+ borderRadius: addFieldRadius,
240
+ alignItems: 'center',
241
+ justifyContent: 'center',
242
+ }
243
+
244
+ // ---- Image / placeholder area --------------------------------------
245
+ const renderCardArea = (): React.ReactNode => {
246
+ if (cardSlot !== undefined && cardSlot !== null) {
247
+ const processed = cloneChildrenWithModes(cardSlot, modes)
248
+ return (
249
+ <View style={imageBoxStyle} pointerEvents="box-none">
250
+ {processed.length === 1 ? processed[0] : processed}
251
+ </View>
252
+ )
253
+ }
254
+
255
+ if (state === 'add') {
256
+ return (
257
+ <View style={addFieldStyle} accessibilityElementsHidden importantForAccessibility="no">
258
+ <Icon name={addIcon} size={addIconSize} color={addIconColor} />
259
+ </View>
260
+ )
261
+ }
262
+
263
+ const normalized = normalizeImageSource(imageSource)
264
+ if (normalized) {
265
+ return (
266
+ <RNImage
267
+ source={normalized}
268
+ style={imageStyle}
269
+ resizeMode="cover"
270
+ accessibilityElementsHidden
271
+ importantForAccessibility="no"
272
+ />
273
+ )
274
+ }
275
+
276
+ return <View style={imageBoxStyle} />
277
+ }
278
+
279
+ // ---- Pressable wiring ----------------------------------------------
280
+ // Keep React state out of the press path. `Pressable`'s style callback
281
+ // applies the pressed visual without a re-render.
282
+ const userHandlersRef = useRef<{
283
+ onPressIn?: (e: any) => void
284
+ onPressOut?: (e: any) => void
285
+ }>({})
286
+
287
+ const handlePressIn = useCallback((e: any) => {
288
+ userHandlersRef.current.onPressIn?.(e)
289
+ }, [])
290
+ const handlePressOut = useCallback((e: any) => {
291
+ userHandlersRef.current.onPressOut?.(e)
292
+ }, [])
293
+
294
+ const containerStyle: ViewStyle = useMemo(
295
+ () => ({
296
+ width: '100%',
297
+ flexDirection: 'column',
298
+ alignItems: 'flex-start',
299
+ gap,
300
+ opacity: disabled ? 0.5 : 1,
301
+ }),
302
+ [gap, disabled]
303
+ )
304
+
305
+ const pressableStyle = useCallback(
306
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
307
+ containerStyle,
308
+ style,
309
+ pressed && !disabled && onPress ? pressedOverlayStyle : null,
310
+ ],
311
+ [containerStyle, style, disabled, onPress]
312
+ )
313
+
314
+ const showSubtitle = state === 'connected' && subtitle != null && subtitle !== ''
315
+
316
+ const a11yRole = onPress ? 'button' : undefined
317
+ const a11yLabel = accessibilityLabel ?? title
318
+
319
+ const content = (
320
+ <>
321
+ {renderCardArea()}
322
+ {(title != null && title !== '') || showSubtitle ? (
323
+ <View
324
+ style={{
325
+ width: '100%',
326
+ flexDirection: 'column',
327
+ alignItems: 'flex-start',
328
+ gap: textWrapGap,
329
+ }}
330
+ >
331
+ {title != null && title !== '' ? (
332
+ <Text style={titleStyle} numberOfLines={1}>
333
+ {title}
334
+ </Text>
335
+ ) : null}
336
+ {showSubtitle ? (
337
+ <Text style={subtitleStyle} numberOfLines={1}>
338
+ {subtitle}
339
+ </Text>
340
+ ) : null}
341
+ </View>
342
+ ) : null}
343
+ </>
344
+ )
345
+
346
+ if (!onPress) {
347
+ return (
348
+ <View
349
+ accessibilityLabel={a11yLabel}
350
+ accessibilityHint={accessibilityHint}
351
+ style={[containerStyle, style]}
352
+ >
353
+ {content}
354
+ </View>
355
+ )
356
+ }
357
+
358
+ return (
359
+ <Pressable
360
+ accessibilityRole={a11yRole}
361
+ accessibilityLabel={a11yLabel}
362
+ accessibilityHint={accessibilityHint}
363
+ accessibilityState={{ disabled }}
364
+ onPress={disabled ? undefined : onPress}
365
+ disabled={disabled}
366
+ onPressIn={handlePressIn}
367
+ onPressOut={handlePressOut}
368
+ unstable_pressDelay={PRESS_DELAY}
369
+ style={pressableStyle}
370
+ >
371
+ {content}
372
+ </Pressable>
373
+ )
374
+ }
375
+
376
+ export default React.memo(AccountCard)