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.
- package/CHANGELOG.md +115 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/PageHero/PageHero.js +189 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Text/Text.js +40 -3
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +67 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
- package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/PageHero/PageHero.js +183 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Text/Text.js +40 -3
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +8 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/Text/Text.d.ts +31 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +8 -1
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/ActionFooter/ActionFooter.tsx +152 -86
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/PageHero/PageHero.tsx +257 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Text/Text.tsx +78 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +16 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { CSSProperties } from 'react'
|
|
3
|
+
|
|
4
|
+
/** Props we forward to the underlying web Lottie view. */
|
|
5
|
+
export type WebLottieViewProps = {
|
|
6
|
+
animationData: Record<string, unknown>
|
|
7
|
+
autoplay?: boolean
|
|
8
|
+
loop?: boolean
|
|
9
|
+
style?: CSSProperties
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const INSTALL_HINT =
|
|
13
|
+
'LottiePlayer requires lottie-react in your app.\n' +
|
|
14
|
+
' npm install lottie-react'
|
|
15
|
+
|
|
16
|
+
function resolveWebLottieModuleName() {
|
|
17
|
+
return ['lottie', '-react'].join('')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function LottieUnavailable(props: WebLottieViewProps) {
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
23
|
+
console.warn(`[jfs-components/LottiePlayer] ${INSTALL_HINT}`)
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
...props.style,
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
justifyContent: 'center',
|
|
34
|
+
backgroundColor: 'rgba(255, 196, 0, 0.12)',
|
|
35
|
+
border: '1px solid rgba(255, 196, 0, 0.45)',
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
padding: 8,
|
|
38
|
+
color: '#8a6d00',
|
|
39
|
+
fontSize: 11,
|
|
40
|
+
textAlign: 'center',
|
|
41
|
+
lineHeight: '15px',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{typeof __DEV__ !== 'undefined' && __DEV__ ? INSTALL_HINT : null}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cachedView: React.ComponentType<WebLottieViewProps> | undefined
|
|
50
|
+
|
|
51
|
+
export function getWebLottieView(): React.ComponentType<WebLottieViewProps> {
|
|
52
|
+
if (cachedView !== undefined) return cachedView
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const mod = require(resolveWebLottieModuleName()) as {
|
|
56
|
+
default?: React.ComponentType<WebLottieViewProps>
|
|
57
|
+
}
|
|
58
|
+
cachedView = mod.default ?? LottieUnavailable
|
|
59
|
+
} catch {
|
|
60
|
+
cachedView = LottieUnavailable
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cachedView
|
|
64
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
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 { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
11
|
+
import Button from '../Button/Button'
|
|
12
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
13
|
+
|
|
14
|
+
export type PageHeroProps = {
|
|
15
|
+
/** Small eyebrow text shown above the headline. */
|
|
16
|
+
eyebrow?: string
|
|
17
|
+
/** Main headline text. Centered and bold. */
|
|
18
|
+
headline?: string
|
|
19
|
+
/** Optional supporting text shown below the headline. */
|
|
20
|
+
supportingText?: string
|
|
21
|
+
/** Whether to render the supporting text. */
|
|
22
|
+
showSupportingText?: boolean
|
|
23
|
+
/** Label for the default action button. Ignored when `buttonSlot` is provided. */
|
|
24
|
+
buttonLabel?: string
|
|
25
|
+
/** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
|
|
26
|
+
onButtonPress?: () => void
|
|
27
|
+
/** Whether to render the default action button. Ignored when `buttonSlot` is provided. */
|
|
28
|
+
showButton?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Optional slot to fully override the action button.
|
|
31
|
+
* When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
|
|
32
|
+
* `modes` are automatically cascaded into this slot.
|
|
33
|
+
*/
|
|
34
|
+
buttonSlot?: React.ReactNode
|
|
35
|
+
/**
|
|
36
|
+
* Optional media element shown above the text block (eyebrow + headline).
|
|
37
|
+
*
|
|
38
|
+
* Intentionally typed as `React.ReactNode` so the consumer can bring any
|
|
39
|
+
* renderer they like — `<Image />`, `<IconCapsule />`, a Lottie player
|
|
40
|
+
* (`lottie-react-native` / `@lottiefiles/dotlottie-react`), an `<SvgXml />`
|
|
41
|
+
* from `react-native-svg`, a `<Video />` from `react-native-video`, a
|
|
42
|
+
* gradient view, or a custom illustration. The library deliberately does
|
|
43
|
+
* NOT wrap Lottie / video runtimes (they require native modules + pod
|
|
44
|
+
* autolinking on every consumer's app), so PageHero just allocates a
|
|
45
|
+
* token-sized container and lets the slot render whatever it wants.
|
|
46
|
+
*
|
|
47
|
+
* The slot is rendered inside a `width × height` box driven by
|
|
48
|
+
* `media/width` and `media/height` tokens (default 117×117). `modes` are
|
|
49
|
+
* automatically cascaded into the slot via `cloneChildrenWithModes`.
|
|
50
|
+
*/
|
|
51
|
+
media?: React.ReactNode
|
|
52
|
+
/** Mode configuration for design-token theming. */
|
|
53
|
+
modes?: Record<string, any>
|
|
54
|
+
/** Style overrides applied to the outer container. */
|
|
55
|
+
style?: StyleProp<ViewStyle>
|
|
56
|
+
testID?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PageHero displays a centered hero block typically used at the top of a page
|
|
61
|
+
* or feature screen. It contains an optional media slot (illustration / image
|
|
62
|
+
* / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
|
|
63
|
+
* headline, an optional supporting line (e.g. price / timeline), and an
|
|
64
|
+
* optional action button.
|
|
65
|
+
*
|
|
66
|
+
* All visual values are resolved from Figma design tokens via
|
|
67
|
+
* `getVariableByName`. Slots cascade the active `modes` to their children
|
|
68
|
+
* through `cloneChildrenWithModes`.
|
|
69
|
+
*
|
|
70
|
+
* @component
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* <PageHero
|
|
74
|
+
* eyebrow="Upgrade to JioFinance+"
|
|
75
|
+
* headline="Resume earning cashback, extra points, and 1% gold"
|
|
76
|
+
* supportingText="₹999/year · ₹0 until 2027"
|
|
77
|
+
* buttonLabel="Renew for free"
|
|
78
|
+
* onButtonPress={() => navigate('Upgrade')}
|
|
79
|
+
* media={
|
|
80
|
+
* <Image
|
|
81
|
+
* imageSource={require('./assets/upgrade.png')}
|
|
82
|
+
* width={117}
|
|
83
|
+
* height={117}
|
|
84
|
+
* />
|
|
85
|
+
* }
|
|
86
|
+
* />
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
function PageHero({
|
|
90
|
+
eyebrow = 'Upgrade to JioFinance+',
|
|
91
|
+
headline = 'Resume earning cashback, extra points, and 1% gold',
|
|
92
|
+
supportingText = '₹999/year · ₹0 until 2027',
|
|
93
|
+
showSupportingText = true,
|
|
94
|
+
buttonLabel = 'Renew for free',
|
|
95
|
+
onButtonPress,
|
|
96
|
+
showButton = true,
|
|
97
|
+
buttonSlot,
|
|
98
|
+
media,
|
|
99
|
+
modes: propModes = EMPTY_MODES,
|
|
100
|
+
style,
|
|
101
|
+
testID,
|
|
102
|
+
}: PageHeroProps) {
|
|
103
|
+
const { modes: globalModes } = useTokens()
|
|
104
|
+
const modes = useMemo(
|
|
105
|
+
() => ({ ...globalModes, ...propModes }),
|
|
106
|
+
[globalModes, propModes]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const gap = Number(getVariableByName('PageHero/gap', modes)) || 16
|
|
110
|
+
const paddingHorizontal =
|
|
111
|
+
Number(getVariableByName('PageHero/padding/horizontal', modes)) || 0
|
|
112
|
+
|
|
113
|
+
const textWrapGap =
|
|
114
|
+
Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
|
|
115
|
+
|
|
116
|
+
// Media slot box — matches the 117×117 frame in Figma (node 4540:7845).
|
|
117
|
+
// Tokens fall back to 117 when not defined in the variables collection,
|
|
118
|
+
// so the layout stays stable on consumers that haven't tokenized this yet.
|
|
119
|
+
const mediaWidth = Number(getVariableByName('media/width', modes)) || 117
|
|
120
|
+
const mediaHeight = Number(getVariableByName('media/height', modes)) || 117
|
|
121
|
+
|
|
122
|
+
const eyebrowColor =
|
|
123
|
+
getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff'
|
|
124
|
+
const eyebrowFontFamily =
|
|
125
|
+
getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System'
|
|
126
|
+
const eyebrowFontSize =
|
|
127
|
+
Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18
|
|
128
|
+
const eyebrowFontWeight =
|
|
129
|
+
getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700
|
|
130
|
+
const eyebrowLineHeight =
|
|
131
|
+
Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20
|
|
132
|
+
|
|
133
|
+
const headlineColor =
|
|
134
|
+
getVariableByName('PageHero/headline/color', modes) || '#ffffff'
|
|
135
|
+
const headlineFontFamily =
|
|
136
|
+
getVariableByName('PageHero/headline/fontFamily', modes) || 'System'
|
|
137
|
+
const headlineFontSize =
|
|
138
|
+
Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29
|
|
139
|
+
const headlineFontWeight =
|
|
140
|
+
getVariableByName('PageHero/headline/fontWeight', modes) || 900
|
|
141
|
+
const headlineLineHeight =
|
|
142
|
+
Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29
|
|
143
|
+
|
|
144
|
+
// Only `lineHeight` is tokenized for the supporting text in the Figma source.
|
|
145
|
+
// Color, font size and weight are inline literals in the design (12px medium
|
|
146
|
+
// white) — we mirror that here so the visual stays faithful when no token
|
|
147
|
+
// exists.
|
|
148
|
+
const supportingTextLineHeight =
|
|
149
|
+
Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16
|
|
150
|
+
|
|
151
|
+
const containerStyle: ViewStyle = {
|
|
152
|
+
flexDirection: 'column',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
paddingHorizontal,
|
|
155
|
+
gap,
|
|
156
|
+
width: '100%',
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const textWrapStyle: ViewStyle = {
|
|
160
|
+
flexDirection: 'column',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
gap: textWrapGap,
|
|
163
|
+
width: '100%',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const eyebrowStyle: TextStyle = {
|
|
167
|
+
color: eyebrowColor as string,
|
|
168
|
+
fontFamily: eyebrowFontFamily as string,
|
|
169
|
+
fontSize: eyebrowFontSize,
|
|
170
|
+
fontWeight: String(eyebrowFontWeight) as TextStyle['fontWeight'],
|
|
171
|
+
lineHeight: eyebrowLineHeight,
|
|
172
|
+
textAlign: 'center',
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const headlineStyle: TextStyle = {
|
|
176
|
+
color: headlineColor as string,
|
|
177
|
+
fontFamily: headlineFontFamily as string,
|
|
178
|
+
fontSize: headlineFontSize,
|
|
179
|
+
fontWeight: String(headlineFontWeight) as TextStyle['fontWeight'],
|
|
180
|
+
lineHeight: headlineLineHeight,
|
|
181
|
+
textAlign: 'center',
|
|
182
|
+
width: '100%',
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const supportingTextStyle: TextStyle = {
|
|
186
|
+
color: '#ffffff',
|
|
187
|
+
fontFamily: 'System',
|
|
188
|
+
fontSize: 12,
|
|
189
|
+
fontWeight: '500',
|
|
190
|
+
lineHeight: supportingTextLineHeight,
|
|
191
|
+
textAlign: 'center',
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const buttonWrapStyle: ViewStyle = {
|
|
195
|
+
width: '100%',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const buttonContent = useMemo<React.ReactNode>(() => {
|
|
199
|
+
if (buttonSlot !== undefined && buttonSlot !== null) {
|
|
200
|
+
return cloneChildrenWithModes(buttonSlot, modes)
|
|
201
|
+
}
|
|
202
|
+
if (!showButton) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
return (
|
|
206
|
+
<Button
|
|
207
|
+
label={buttonLabel}
|
|
208
|
+
onPress={onButtonPress}
|
|
209
|
+
modes={modes}
|
|
210
|
+
style={buttonWrapStyle}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
// buttonWrapStyle is a literal object created above; it is intentionally
|
|
214
|
+
// omitted from deps because its identity changes on every render but its
|
|
215
|
+
// shape never does.
|
|
216
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
217
|
+
}, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
|
|
218
|
+
|
|
219
|
+
// Sized container for the media slot. Always rendered when `media` is
|
|
220
|
+
// provided, so the slot has a predictable box (matches Figma frame
|
|
221
|
+
// 4540:7845 — 117×117 by default) even if the inner element omits its
|
|
222
|
+
// own width/height. `overflow: 'hidden'` mirrors the Figma frame's
|
|
223
|
+
// `clipsContent` so a slightly oversized illustration doesn't break
|
|
224
|
+
// the centered layout.
|
|
225
|
+
const mediaContent = useMemo<React.ReactNode>(() => {
|
|
226
|
+
if (media === undefined || media === null) return null
|
|
227
|
+
return (
|
|
228
|
+
<View
|
|
229
|
+
style={{
|
|
230
|
+
width: mediaWidth,
|
|
231
|
+
height: mediaHeight,
|
|
232
|
+
alignItems: 'center',
|
|
233
|
+
justifyContent: 'center',
|
|
234
|
+
overflow: 'hidden',
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
{cloneChildrenWithModes(media, modes)}
|
|
238
|
+
</View>
|
|
239
|
+
)
|
|
240
|
+
}, [media, mediaWidth, mediaHeight, modes])
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<View style={[containerStyle, style]} testID={testID}>
|
|
244
|
+
{mediaContent}
|
|
245
|
+
<View style={textWrapStyle}>
|
|
246
|
+
{eyebrow ? <Text style={eyebrowStyle}>{eyebrow}</Text> : null}
|
|
247
|
+
{headline ? <Text style={headlineStyle}>{headline}</Text> : null}
|
|
248
|
+
</View>
|
|
249
|
+
{showSupportingText && supportingText ? (
|
|
250
|
+
<Text style={supportingTextStyle}>{supportingText}</Text>
|
|
251
|
+
) : null}
|
|
252
|
+
{buttonContent}
|
|
253
|
+
</View>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default PageHero
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Text,
|
|
4
|
+
View,
|
|
5
|
+
type ImageStyle,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native'
|
|
10
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
11
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
12
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
13
|
+
import MediaSource, { type UnifiedSource } from '../../utils/MediaSource'
|
|
14
|
+
|
|
15
|
+
// Default bundled FINVU brand logo, matching the Figma reference so the
|
|
16
|
+
// component renders correctly out of the box without any image prop.
|
|
17
|
+
const DEFAULT_LOGO: UnifiedSource = require('./finvu.png')
|
|
18
|
+
|
|
19
|
+
const DEFAULT_LABEL = 'Powered by RBI-regulated account aggregator'
|
|
20
|
+
const DEFAULT_IMAGE_WIDTH = 33
|
|
21
|
+
const DEFAULT_IMAGE_HEIGHT = 12
|
|
22
|
+
|
|
23
|
+
export type PoweredByLabelProps = {
|
|
24
|
+
/**
|
|
25
|
+
* Label copy. Defaults to "Powered by RBI-regulated account aggregator"
|
|
26
|
+
* to match the Figma reference.
|
|
27
|
+
*/
|
|
28
|
+
label?: string
|
|
29
|
+
/**
|
|
30
|
+
* Brand logo source. Accepts any {@link UnifiedSource} — remote URI, SVG
|
|
31
|
+
* XML string, `require()` asset, SVG React component, or React element.
|
|
32
|
+
* Defaults to the bundled FINVU logo so the component renders correctly
|
|
33
|
+
* without any caller-provided image.
|
|
34
|
+
*
|
|
35
|
+
* Ignored when `imageSlot` is provided.
|
|
36
|
+
*/
|
|
37
|
+
imageSource?: UnifiedSource
|
|
38
|
+
/**
|
|
39
|
+
* Rendered width of the logo in px. Defaults to `33` to match Figma.
|
|
40
|
+
*/
|
|
41
|
+
imageWidth?: number
|
|
42
|
+
/**
|
|
43
|
+
* Rendered height of the logo in px. Defaults to `12` to match Figma.
|
|
44
|
+
*/
|
|
45
|
+
imageHeight?: number
|
|
46
|
+
/**
|
|
47
|
+
* Replace the default `Image` entirely (e.g. with a vector logo,
|
|
48
|
+
* `BrandChip`, or custom layout). Receives `modes` recursively.
|
|
49
|
+
* Overrides `imageSource`.
|
|
50
|
+
*/
|
|
51
|
+
imageSlot?: React.ReactNode
|
|
52
|
+
/**
|
|
53
|
+
* Design token modes for theming (e.g. `{ 'Color Mode': 'Dark' }`).
|
|
54
|
+
*/
|
|
55
|
+
modes?: Record<string, any>
|
|
56
|
+
/** Container style override. */
|
|
57
|
+
style?: StyleProp<ViewStyle>
|
|
58
|
+
/** Label text style override. */
|
|
59
|
+
textStyle?: StyleProp<TextStyle>
|
|
60
|
+
/** Logo image style override (size/resize behaviour, etc.). */
|
|
61
|
+
imageStyle?: StyleProp<ImageStyle>
|
|
62
|
+
/**
|
|
63
|
+
* Accessibility label. Defaults to the visible `label` so the brand image
|
|
64
|
+
* (which is decorative) does not need to be announced separately.
|
|
65
|
+
*/
|
|
66
|
+
accessibilityLabel?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const toNumber = (value: unknown, fallback: number): number => {
|
|
70
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
const parsed = Number(value)
|
|
73
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
74
|
+
}
|
|
75
|
+
return fallback
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toFontWeight = (
|
|
79
|
+
value: unknown,
|
|
80
|
+
fallback: TextStyle['fontWeight']
|
|
81
|
+
): TextStyle['fontWeight'] => {
|
|
82
|
+
if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
|
|
83
|
+
if (typeof value === 'string') return value as TextStyle['fontWeight']
|
|
84
|
+
return fallback
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `PoweredByLabel` renders the small "Powered by RBI-regulated account
|
|
89
|
+
* aggregator" badge with a trailing brand logo, used to attribute the
|
|
90
|
+
* underlying account-aggregator partner in flows such as bank-account
|
|
91
|
+
* linking and consent screens.
|
|
92
|
+
*
|
|
93
|
+
* The component is composed of:
|
|
94
|
+
*
|
|
95
|
+
* 1. A token-styled pill container (`poweredByLabel/background`,
|
|
96
|
+
* `poweredByLabel/padding/*`).
|
|
97
|
+
* 2. The disclosure copy rendered through the `poweredByLabel/*` typography
|
|
98
|
+
* tokens.
|
|
99
|
+
* 3. A configurable brand logo slot. Defaults to the bundled FINVU mark, but
|
|
100
|
+
* callers can pass any image via `imageSource` or fully replace the slot
|
|
101
|
+
* via `imageSlot`.
|
|
102
|
+
*
|
|
103
|
+
* @component
|
|
104
|
+
* @param {PoweredByLabelProps} props
|
|
105
|
+
*/
|
|
106
|
+
function PoweredByLabel({
|
|
107
|
+
label = DEFAULT_LABEL,
|
|
108
|
+
imageSource,
|
|
109
|
+
imageWidth = DEFAULT_IMAGE_WIDTH,
|
|
110
|
+
imageHeight = DEFAULT_IMAGE_HEIGHT,
|
|
111
|
+
imageSlot,
|
|
112
|
+
modes: propModes = EMPTY_MODES,
|
|
113
|
+
style,
|
|
114
|
+
textStyle,
|
|
115
|
+
imageStyle,
|
|
116
|
+
accessibilityLabel,
|
|
117
|
+
}: PoweredByLabelProps) {
|
|
118
|
+
const { modes: globalModes } = useTokens()
|
|
119
|
+
const modes = useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
122
|
+
? EMPTY_MODES
|
|
123
|
+
: { ...globalModes, ...propModes },
|
|
124
|
+
[globalModes, propModes]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const background =
|
|
128
|
+
(getVariableByName('poweredByLabel/background', modes) as string | null) ??
|
|
129
|
+
'#f5f5f5'
|
|
130
|
+
const foreground =
|
|
131
|
+
(getVariableByName('poweredByLabel/foreground', modes) as string | null) ??
|
|
132
|
+
'#191b1e'
|
|
133
|
+
const fontFamily =
|
|
134
|
+
(getVariableByName('poweredByLabel/fontFamily', modes) as string | null) ??
|
|
135
|
+
'JioType Var'
|
|
136
|
+
const fontSize = toNumber(getVariableByName('poweredByLabel/fontSize', modes), 10)
|
|
137
|
+
const lineHeight = toNumber(
|
|
138
|
+
getVariableByName('poweredByLabel/lineHeight', modes),
|
|
139
|
+
12
|
|
140
|
+
)
|
|
141
|
+
const fontWeight = toFontWeight(
|
|
142
|
+
getVariableByName('poweredByLabel/fontWeight', modes),
|
|
143
|
+
'400'
|
|
144
|
+
)
|
|
145
|
+
const gap = toNumber(getVariableByName('poweredByLabel/gap', modes), 10)
|
|
146
|
+
const paddingHorizontal = toNumber(
|
|
147
|
+
getVariableByName('poweredByLabel/padding/horizontal', modes),
|
|
148
|
+
16
|
|
149
|
+
)
|
|
150
|
+
const paddingVertical = toNumber(
|
|
151
|
+
getVariableByName('poweredByLabel/padding/vertical', modes),
|
|
152
|
+
6
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const containerStyle: ViewStyle = {
|
|
156
|
+
flexDirection: 'row',
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
backgroundColor: background,
|
|
160
|
+
paddingHorizontal,
|
|
161
|
+
paddingVertical,
|
|
162
|
+
gap,
|
|
163
|
+
// Hug content horizontally so the pill does not stretch to fill the
|
|
164
|
+
// parent (matches Badge, BrandChip, etc.). Override via `style` if
|
|
165
|
+
// you want it full-width (e.g. inside a card footer).
|
|
166
|
+
alignSelf: 'flex-start',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const labelTextStyle: TextStyle = {
|
|
170
|
+
color: foreground,
|
|
171
|
+
fontFamily,
|
|
172
|
+
fontSize,
|
|
173
|
+
lineHeight,
|
|
174
|
+
fontWeight,
|
|
175
|
+
textAlign: 'center',
|
|
176
|
+
flexShrink: 1,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const renderImage = (): React.ReactNode => {
|
|
180
|
+
if (imageSlot !== undefined && imageSlot !== null) {
|
|
181
|
+
const processed = cloneChildrenWithModes(imageSlot, modes)
|
|
182
|
+
if (processed.length === 0) return null
|
|
183
|
+
return processed.length === 1 ? processed[0] : processed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const resolvedSource: UnifiedSource =
|
|
187
|
+
(imageSource as UnifiedSource | undefined) ?? DEFAULT_LOGO
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<MediaSource
|
|
191
|
+
source={resolvedSource}
|
|
192
|
+
width={imageWidth}
|
|
193
|
+
height={imageHeight}
|
|
194
|
+
resizeMode="contain"
|
|
195
|
+
style={imageStyle}
|
|
196
|
+
accessibilityElementsHidden={true}
|
|
197
|
+
importantForAccessibility="no"
|
|
198
|
+
/>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<View
|
|
204
|
+
accessibilityRole="text"
|
|
205
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
206
|
+
style={[containerStyle, style]}
|
|
207
|
+
>
|
|
208
|
+
<Text
|
|
209
|
+
style={[labelTextStyle, textStyle]}
|
|
210
|
+
accessibilityElementsHidden={true}
|
|
211
|
+
importantForAccessibility="no"
|
|
212
|
+
numberOfLines={1}
|
|
213
|
+
>
|
|
214
|
+
{label}
|
|
215
|
+
</Text>
|
|
216
|
+
{renderImage()}
|
|
217
|
+
</View>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default PoweredByLabel
|
|
Binary file
|
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { View, Text,
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, type ViewStyle } from 'react-native';
|
|
3
3
|
import ButtonGroup from '../ButtonGroup/ButtonGroup';
|
|
4
4
|
import AvatarGroup from '../AvatarGroup/AvatarGroup';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
7
|
import MoneyValue from '../MoneyValue/MoneyValue';
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
// Defaults applied to the inner ButtonGroup so the card matches Figma out of
|
|
10
|
+
// the box. They are spread *before* the caller's `modes` so any consumer can
|
|
11
|
+
// override an individual key (e.g. swap the size to "M").
|
|
12
|
+
const DEFAULT_BUTTON_GROUP_MODES: Record<string, string> = {
|
|
13
|
+
AppearanceBrand: 'Secondary',
|
|
14
|
+
'Button / Size': 'S',
|
|
15
|
+
Emphasis: 'High',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Defaults applied to the inner MoneyValue so the price renders at the
|
|
19
|
+
// 36 px / 900-weight scale defined for cards in Figma. Same merge rule as
|
|
20
|
+
// the button group — consumer `modes` override these.
|
|
21
|
+
const DEFAULT_MONEY_VALUE_MODES: Record<string, string> = {
|
|
22
|
+
Context3: 'Balance & Cards',
|
|
23
|
+
};
|
|
9
24
|
|
|
10
25
|
|
|
11
26
|
export type RechargeCardProps = {
|
|
@@ -66,17 +81,19 @@ export default function RechargeCard({
|
|
|
66
81
|
modes = EMPTY_MODES,
|
|
67
82
|
style,
|
|
68
83
|
}: RechargeCardProps) {
|
|
69
|
-
// Container Tokens
|
|
70
|
-
const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#
|
|
84
|
+
// Container Tokens (defaults mirror Figma node 2235:937).
|
|
85
|
+
const backgroundColor = getVariableByName('rechargeCard/background', modes) || '#ffffff';
|
|
71
86
|
const paddingHorizontal = parseInt(getVariableByName('rechargeCard/padding/horizontal', modes) || 16, 10);
|
|
72
87
|
const paddingVertical = parseInt(getVariableByName('rechargeCard/padding/vertical', modes) || 20, 10);
|
|
73
88
|
const gap = parseInt(getVariableByName('rechargeCard/gap', modes) || 20, 10);
|
|
74
89
|
const radius = parseInt(getVariableByName('rechargeCard/radius', modes) || 20, 10);
|
|
75
|
-
const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) ||
|
|
90
|
+
const minWidth = parseInt(getVariableByName('rechargeCard/minWidth', modes) || 312, 10);
|
|
91
|
+
const strokeWidth = parseInt(getVariableByName('rechargeCard/strokeWidth', modes) || 1, 10);
|
|
92
|
+
const strokeColor = getVariableByName('rechargeCard/stroke/color', modes) || '#ebebed';
|
|
76
93
|
|
|
77
94
|
// Header Tokens
|
|
78
95
|
const headerGap = parseInt(getVariableByName('rechargeCard/header/gap', modes) || 4, 10);
|
|
79
|
-
const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#
|
|
96
|
+
const titleColor = getVariableByName('rechargeCard/title/color', modes) || '#000000';
|
|
80
97
|
const titleFontSize = parseInt(getVariableByName('rechargeCard/title/fontSize', modes) || 12, 10);
|
|
81
98
|
const titleFontFamily = getVariableByName('rechargeCard/title/fontFamily', modes) || 'JioType Var';
|
|
82
99
|
const titleLineHeight = parseInt(getVariableByName('rechargeCard/title/lineHeight', modes) || 14, 10);
|
|
@@ -87,30 +104,26 @@ export default function RechargeCard({
|
|
|
87
104
|
const specItemGap = parseInt(getVariableByName('rechargeCard/specItem/gap', modes) || 4, 10);
|
|
88
105
|
|
|
89
106
|
// Spec Label Tokens
|
|
90
|
-
const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#
|
|
107
|
+
const specLabelColor = getVariableByName('rechargeCard/specItem/label/color', modes) || '#000000';
|
|
91
108
|
const specLabelFontSize = parseInt(getVariableByName('rechargeCard/specItem/label/fontSize', modes) || 12, 10);
|
|
92
109
|
const specLabelFontFamily = getVariableByName('rechargeCard/specItem/label/fontFamily', modes) || 'JioType Var';
|
|
93
110
|
const specLabelLineHeight = parseInt(getVariableByName('rechargeCard/specItem/label/lineHeight', modes) || 14, 10);
|
|
94
111
|
const specLabelFontWeight = getVariableByName('rechargeCard/specItem/label/fontWeight', modes) || '500';
|
|
95
112
|
|
|
96
113
|
// Spec Value Tokens
|
|
97
|
-
const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#
|
|
114
|
+
const specValueColor = getVariableByName('rechargeCard/specItem/value/color', modes) || '#000000';
|
|
98
115
|
const specValueFontSize = parseInt(getVariableByName('rechargeCard/specItem/value/fontSize', modes) || 14, 10);
|
|
99
116
|
const specValueFontFamily = getVariableByName('rechargeCard/specItem/value/fontFamily', modes) || 'JioType Var';
|
|
100
117
|
const specValueLineHeight = parseInt(getVariableByName('rechargeCard/specItem/value/lineHeight', modes) || 17, 10);
|
|
101
118
|
const specValueFontWeight = getVariableByName('rechargeCard/specItem/value/fontWeight', modes) || '500';
|
|
102
119
|
|
|
103
120
|
// Disclaimer Tokens
|
|
104
|
-
const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#
|
|
121
|
+
const disclaimerColor = getVariableByName('rechargeCard/disclaimer/color', modes) || '#000000';
|
|
105
122
|
const disclaimerFontSize = parseInt(getVariableByName('rechargeCard/disclaimer/fontSize', modes) || 10, 10);
|
|
106
123
|
const disclaimerFontFamily = getVariableByName('rechargeCard/disclaimer/fontFamily', modes) || 'JioType Var';
|
|
107
124
|
const disclaimerLineHeight = parseInt(getVariableByName('rechargeCard/disclaimer/lineHeight', modes) || 13, 10);
|
|
108
125
|
const disclaimerFontWeight = getVariableByName('rechargeCard/disclaimer/fontWeight', modes) || '400';
|
|
109
126
|
|
|
110
|
-
// Button Group Tokens
|
|
111
|
-
// Handled by ButtonGroup component directly
|
|
112
|
-
|
|
113
|
-
// Helpers
|
|
114
127
|
const resolveFontWeight = (weight: string | number) => typeof weight === 'number' ? weight.toString() : weight;
|
|
115
128
|
|
|
116
129
|
// Pass modes to subscription children (e.g. AvatarGroup)
|
|
@@ -125,6 +138,8 @@ export default function RechargeCard({
|
|
|
125
138
|
paddingVertical,
|
|
126
139
|
gap,
|
|
127
140
|
borderRadius: radius,
|
|
141
|
+
borderWidth: strokeWidth,
|
|
142
|
+
borderColor: strokeColor,
|
|
128
143
|
minWidth,
|
|
129
144
|
alignItems: 'flex-start',
|
|
130
145
|
}, style]}>
|
|
@@ -142,7 +157,7 @@ export default function RechargeCard({
|
|
|
142
157
|
<MoneyValue
|
|
143
158
|
value={price}
|
|
144
159
|
currency="₹"
|
|
145
|
-
modes={modes}
|
|
160
|
+
modes={{ ...DEFAULT_MONEY_VALUE_MODES, ...modes }}
|
|
146
161
|
/>
|
|
147
162
|
</View>
|
|
148
163
|
|
|
@@ -224,15 +239,8 @@ export default function RechargeCard({
|
|
|
224
239
|
{disclaimer}
|
|
225
240
|
</Text>
|
|
226
241
|
|
|
227
|
-
{/* Button Group */}
|
|
228
|
-
<ButtonGroup
|
|
229
|
-
modes={{
|
|
230
|
-
...modes,
|
|
231
|
-
"Appearance.Brand": "Secondary",
|
|
232
|
-
"Button / Size": "S",
|
|
233
|
-
"Emphasis": "High"
|
|
234
|
-
}}
|
|
235
|
-
>
|
|
242
|
+
{/* Button Group: defaults are overridable via the consumer's `modes` */}
|
|
243
|
+
<ButtonGroup modes={{ ...DEFAULT_BUTTON_GROUP_MODES, ...modes }}>
|
|
236
244
|
{actions}
|
|
237
245
|
</ButtonGroup>
|
|
238
246
|
</View>
|