jfs-components 0.0.73 → 0.0.77

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 (134) hide show
  1. package/CHANGELOG.md +115 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  6. package/lib/commonjs/components/Badge/Badge.js +23 -0
  7. package/lib/commonjs/components/Button/Button.js +37 -0
  8. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  10. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  12. package/lib/commonjs/components/FormField/FormField.js +328 -178
  13. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  14. package/lib/commonjs/components/Image/Image.js +26 -1
  15. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  17. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  18. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  19. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  20. package/lib/commonjs/components/PageHero/PageHero.js +189 -0
  21. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  22. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  23. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  24. package/lib/commonjs/components/Text/Text.js +40 -3
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  26. package/lib/commonjs/components/index.js +67 -0
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/Icon.js +16 -0
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/commonjs/index.js +12 -0
  31. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  32. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  33. package/lib/commonjs/skeleton/index.js +58 -0
  34. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  35. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  36. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  37. package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
  38. package/lib/module/components/AppBar/AppBar.js +17 -11
  39. package/lib/module/components/Avatar/Avatar.js +19 -0
  40. package/lib/module/components/Badge/Badge.js +23 -0
  41. package/lib/module/components/Button/Button.js +37 -0
  42. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  43. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  44. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  45. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  46. package/lib/module/components/FormField/FormField.js +330 -180
  47. package/lib/module/components/IconButton/IconButton.js +20 -0
  48. package/lib/module/components/Image/Image.js +25 -1
  49. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  50. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  51. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  52. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  53. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  54. package/lib/module/components/PageHero/PageHero.js +183 -0
  55. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  56. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  57. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  58. package/lib/module/components/Text/Text.js +40 -3
  59. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  60. package/lib/module/components/index.js +8 -1
  61. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  62. package/lib/module/icons/Icon.js +16 -0
  63. package/lib/module/icons/registry.js +1 -1
  64. package/lib/module/index.js +2 -1
  65. package/lib/module/skeleton/Skeleton.js +229 -0
  66. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  67. package/lib/module/skeleton/index.js +6 -0
  68. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  69. package/lib/module/skeleton/useReducedMotion.js +61 -0
  70. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  71. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  72. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  73. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  74. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  75. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  76. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  77. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  78. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  79. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  80. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  81. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  82. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  83. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  84. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  85. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  86. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  87. package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
  88. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  89. package/lib/typescript/src/components/Text/Text.d.ts +31 -2
  90. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  91. package/lib/typescript/src/components/index.d.ts +8 -1
  92. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  93. package/lib/typescript/src/icons/registry.d.ts +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -0
  95. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  96. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  97. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  98. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  99. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  100. package/package.json +11 -3
  101. package/src/components/AccountCard/AccountCard.tsx +376 -0
  102. package/src/components/ActionFooter/ActionFooter.tsx +152 -86
  103. package/src/components/AppBar/AppBar.tsx +25 -14
  104. package/src/components/Avatar/Avatar.tsx +26 -0
  105. package/src/components/Badge/Badge.tsx +27 -0
  106. package/src/components/Button/Button.tsx +40 -0
  107. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  108. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  109. package/src/components/Dropdown/Dropdown.tsx +331 -0
  110. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  111. package/src/components/FormField/FormField.tsx +542 -215
  112. package/src/components/IconButton/IconButton.tsx +27 -0
  113. package/src/components/Image/Image.tsx +25 -0
  114. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  115. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  116. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  117. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  118. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  119. package/src/components/PageHero/PageHero.tsx +257 -0
  120. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  121. package/src/components/PoweredByLabel/finvu.png +0 -0
  122. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  123. package/src/components/Text/Text.tsx +78 -3
  124. package/src/components/Tooltip/Tooltip.tsx +50 -25
  125. package/src/components/index.ts +16 -1
  126. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  127. package/src/icons/Icon.tsx +17 -0
  128. package/src/icons/registry.ts +1 -1
  129. package/src/index.ts +1 -0
  130. package/src/skeleton/Skeleton.tsx +298 -0
  131. package/src/skeleton/SkeletonGroup.tsx +193 -0
  132. package/src/skeleton/index.ts +10 -0
  133. package/src/skeleton/shimmer-tokens.ts +221 -0
  134. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Single source of truth for the skeleton shimmer behaviour spec.
3
+ *
4
+ * The two modes mirror the documented design behaviour:
5
+ *
6
+ * Normal:
7
+ * - 1.5s linear sawtooth cycle (band sweeps end-to-end then resets)
8
+ * - 135° gradient (top-left -> bottom-right), fixed angle on any aspect
9
+ * ratio via SVG userSpaceOnUse coordinates
10
+ * - Band alpha animates 33% -> 100% -> 33% across the band core;
11
+ * transparent fades at the gradient edges hide the sawtooth reset
12
+ * - 50–100ms per-item stagger
13
+ *
14
+ * Reduced motion (system-level OS toggle):
15
+ * - 3.0s ease-in-out cycle
16
+ * - No translation — solid white overlay opacity pulses 33% -> 100% in
17
+ * place (`opacityRange` below is consumed in this path)
18
+ * - No gradient
19
+ * - No stagger
20
+ *
21
+ * Centralising the spec here keeps `Skeleton.tsx` purely presentational and
22
+ * lets us tune the behaviour in one place if design ever updates it.
23
+ */
24
+ export type SkeletonKind = 'text' | 'image' | 'badge' | 'other';
25
+ export interface ShimmerModeSpec {
26
+ durationMs: number;
27
+ /** Per-item delay range. Min/max in milliseconds. */
28
+ staggerMsRange: readonly [number, number];
29
+ gradient: boolean;
30
+ /** Opacity range applied to the moving "blob" overlay. */
31
+ opacityRange: readonly [number, number];
32
+ }
33
+ export declare const SHIMMER: {
34
+ readonly normal: {
35
+ durationMs: number;
36
+ staggerMsRange: readonly [50, 100];
37
+ gradient: true;
38
+ opacityRange: readonly [0.33, 1];
39
+ };
40
+ readonly reduced: {
41
+ durationMs: number;
42
+ staggerMsRange: readonly [0, 0];
43
+ gradient: false;
44
+ opacityRange: readonly [0.33, 1];
45
+ };
46
+ /**
47
+ * Gradient angle in degrees, measured the CSS way: 0deg points "up", 90deg
48
+ * "right", 135deg therefore points down-right (top-left corner to
49
+ * bottom-right corner).
50
+ */
51
+ readonly gradientAngleDeg: 135;
52
+ /**
53
+ * Hard cap on the cumulative stagger delay so very long lists don't drift
54
+ * out forever; the wave wraps after this many ms.
55
+ */
56
+ readonly maxStaggerMs: 600;
57
+ };
58
+ /**
59
+ * Token names — referenced via `getVariableByName(...)` so the existing
60
+ * design-token resolver does its job (caching, mode resolution, etc.).
61
+ *
62
+ * The four tokens already live in the Figma export at
63
+ * `src/design-tokens/Coin Variables-variables-full.json`:
64
+ *
65
+ * - `bg/defaultSkeleton` -> base color for every skeleton block
66
+ * - `cornerRadius/defaultSkeleton` -> text & "other" (pill: 9999)
67
+ * - `cornerRadius/imageSkeleton` -> images (10)
68
+ * - `cornerRadius/badgeSkeleton` -> badges (4)
69
+ */
70
+ export declare const SKELETON_TOKEN: {
71
+ readonly background: "bg/defaultSkeleton";
72
+ readonly radius: {
73
+ text: string;
74
+ image: string;
75
+ badge: string;
76
+ other: string;
77
+ };
78
+ };
79
+ /**
80
+ * Fallback constants used when the token resolver is unavailable (tests,
81
+ * SSR, etc.). Match the current Figma values.
82
+ */
83
+ export declare const SKELETON_FALLBACK: {
84
+ readonly backgroundColor: "rgb(245, 245, 246)";
85
+ readonly radius: {
86
+ text: number;
87
+ image: number;
88
+ badge: number;
89
+ other: number;
90
+ };
91
+ };
92
+ /**
93
+ * Compute a stable per-item delay from a 0-based registration index.
94
+ *
95
+ * We pick the midpoint of the documented range (75ms) so the cascade is
96
+ * deterministic for snapshot tests and visually identical between renders.
97
+ * The total delay is then wrapped at `maxStaggerMs` so deep lists don't
98
+ * drift past the cap.
99
+ */
100
+ export declare function staggerDelayMs(index: number, mode: ShimmerModeSpec): number;
101
+ /** One stop on the moving shimmer gradient (offset 0–1 along the 135° axis). */
102
+ export interface ShimmerGradientStop {
103
+ offset: number;
104
+ opacity: number;
105
+ }
106
+ /**
107
+ * Gradient stops for the normal-mode moving band. The peak sits at 0.5; fully
108
+ * transparent tails at 0 and 1. The 0.30 / 0.70 stops mark where the band
109
+ * reaches the documented 33 % alpha.
110
+ */
111
+ export declare const SHIMMER_GRADIENT_STOPS: readonly ShimmerGradientStop[];
112
+ /** Offset (0–1) of the brightest point on the gradient line. */
113
+ export declare function gradientPeakOffset(stops?: readonly ShimmerGradientStop[]): number;
114
+ /**
115
+ * How far the gradient extends from the peak to a fully transparent stop,
116
+ * expressed as a fraction of the gradient line (0–1). With the default stops
117
+ * this is 0.5 (peak at 0.5, transparent at 0 and 1).
118
+ *
119
+ * This value drives the overshoot: the band must travel this fraction of a
120
+ * full corner-to-corner sweep *beyond* each corner so the soft transparent
121
+ * tails fully clear the box before the sawtooth reset.
122
+ */
123
+ export declare function gradientTransparentExtent(stops?: readonly ShimmerGradientStop[]): number;
124
+ export interface ShimmerMotionGeometry {
125
+ /** Square overlay side length (gradient is painted on this). */
126
+ overlaySize: number;
127
+ /** Padding to centre the overlay on the box. */
128
+ padX: number;
129
+ padY: number;
130
+ /** Translation (k, k) when the band is fully off-screen before entry. */
131
+ kStart: number;
132
+ /** Translation (k, k) when the band is fully off-screen after exit. */
133
+ kEnd: number;
134
+ /**
135
+ * Normalised overshoot on each side of the corner-to-corner sweep. A value
136
+ * of 0.5 means the travel extends 50 % past each corner; at reset the
137
+ * clock jumps from `1 + overshoot` to `-overshoot` while the box still
138
+ * shows only the base skeleton colour.
139
+ */
140
+ overshootFraction: number;
141
+ /** k distance from peak-at-TL to peak-at-BR. */
142
+ cornerTravelK: number;
143
+ /** k distance from peak to fully transparent tail on the gradient. */
144
+ fadeBeyondPeakK: number;
145
+ }
146
+ /**
147
+ * Derive the moving-shimmer geometry from box size, overlay size, and the
148
+ * gradient stop layout.
149
+ *
150
+ * Coordinate model (135° / down-right):
151
+ * - Peak stripe world diagonal sum: (W + H) / 2 + 2k
152
+ * - Gradient offset 0 → 1 spans 2 × overlaySize in diagonal-sum units
153
+ * - Transparent tail beyond peak: fadeExtent × 2 × overlaySize / 2
154
+ * = fadeExtent × overlaySize in k units
155
+ *
156
+ * The sawtooth clock maps linearly kStart → kEnd so t = 0 and t = 1 both
157
+ * land with the entire gradient outside the box — no jitter on reset.
158
+ */
159
+ export declare function computeShimmerMotion(width: number, height: number, overlaySize: number, stops?: readonly ShimmerGradientStop[]): ShimmerMotionGeometry | null;
160
+ //# sourceMappingURL=shimmer-tokens.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cross-platform "prefers reduced motion" hook.
3
+ *
4
+ * - Native: reads `AccessibilityInfo.isReduceMotionEnabled()` and subscribes
5
+ * to `reduceMotionChanged` events so the value stays live as the user
6
+ * toggles the OS setting.
7
+ * - Web: uses `window.matchMedia('(prefers-reduced-motion: reduce)')`,
8
+ * subscribing to its `change` event.
9
+ * - Anywhere either API is missing: returns `false` (no reduction).
10
+ *
11
+ * The hook never throws — every native API access is defensively guarded so
12
+ * the skeleton system stays safe in tests, SSR, and constrained sandboxes.
13
+ */
14
+ export declare function useReducedMotion(): boolean;
15
+ //# sourceMappingURL=useReducedMotion.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.73",
3
+ "version": "0.0.77",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -36,8 +36,6 @@
36
36
  "storybook-web": "storybook dev -p 6006",
37
37
  "build:storybook-web": "storybook build",
38
38
  "deploy:storybook": "vercel --prod",
39
- "example": "yarn --cwd example start",
40
- "example:install": "yarn --cwd example install",
41
39
  "postinstall": "patch-package",
42
40
  "icons:generate": "node scripts/generate-icon-registry.js",
43
41
  "prepare": "bob build",
@@ -96,9 +94,19 @@
96
94
  },
97
95
  "peerDependencies": {
98
96
  "@react-native-community/blur": ">=4.4.0",
97
+ "lottie-react": ">=2.4.0",
98
+ "lottie-react-native": ">=7.0.0",
99
99
  "react": "*",
100
100
  "react-native": "*"
101
101
  },
102
+ "peerDependenciesMeta": {
103
+ "lottie-react": {
104
+ "optional": true
105
+ },
106
+ "lottie-react-native": {
107
+ "optional": true
108
+ }
109
+ },
102
110
  "devDependencies": {
103
111
  "@babel/core": "^7.28.5",
104
112
  "@babel/parser": "^7.28.5",
@@ -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)