jfs-components 0.0.43 → 0.0.45
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/lib/commonjs/components/AmountInput/AmountInput.js +82 -0
- package/lib/commonjs/components/AmountInput/index.js +13 -0
- package/lib/commonjs/components/Button/Button.js +45 -28
- package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +76 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +208 -0
- package/lib/commonjs/components/EmptyState/EmptyState.js +2 -1
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +81 -49
- package/lib/commonjs/components/NoteInput/NoteInput.js +120 -0
- package/lib/commonjs/components/NoteInput/index.js +13 -0
- package/lib/commonjs/components/Numpad/Numpad.js +108 -0
- package/lib/commonjs/components/OTP/OTP.js +242 -0
- package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +78 -0
- package/lib/commonjs/components/ProductLabel/ProductLabel.js +50 -0
- package/lib/commonjs/components/ProgressBadge/ProgressBadge.js +130 -0
- package/lib/commonjs/components/ProgressBadge/index.js +25 -0
- package/lib/commonjs/components/StatItem/StatItem.js +61 -0
- package/lib/commonjs/components/StatusHero/StatusHero.js +148 -0
- package/lib/commonjs/components/SwappableAmount/SwappableAmount.js +71 -0
- package/lib/commonjs/components/Tabs/TabItem.js +79 -0
- package/lib/commonjs/components/Tabs/Tabs.js +88 -0
- package/lib/commonjs/components/Text/Text.js +38 -0
- package/lib/commonjs/components/Toggle/Toggle.js +102 -0
- package/lib/commonjs/components/index.js +105 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -0
- package/lib/commonjs/design-tokens/figma-variables-resolver.js +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AmountInput/AmountInput.js +77 -0
- package/lib/module/components/AmountInput/index.js +3 -0
- package/lib/module/components/Button/Button.js +44 -28
- package/lib/module/components/CardProviderInfo/CardProviderInfo.js +71 -0
- package/lib/module/components/Checkbox/Checkbox.js +205 -0
- package/lib/module/components/EmptyState/EmptyState.js +2 -1
- package/lib/module/components/MoneyValue/MoneyValue.js +81 -49
- package/lib/module/components/NoteInput/NoteInput.js +115 -0
- package/lib/module/components/NoteInput/index.js +3 -0
- package/lib/module/components/Numpad/Numpad.js +103 -0
- package/lib/module/components/OTP/OTP.js +236 -0
- package/lib/module/components/PortfolioHero/PortfolioHero.js +73 -0
- package/lib/module/components/ProductLabel/ProductLabel.js +45 -0
- package/lib/module/components/ProgressBadge/ProgressBadge.js +125 -0
- package/lib/module/components/ProgressBadge/index.js +4 -0
- package/lib/module/components/StatItem/StatItem.js +56 -0
- package/lib/module/components/StatusHero/StatusHero.js +142 -0
- package/lib/module/components/SwappableAmount/SwappableAmount.js +66 -0
- package/lib/module/components/Tabs/TabItem.js +74 -0
- package/lib/module/components/Tabs/Tabs.js +78 -0
- package/lib/module/components/Text/Text.js +33 -0
- package/lib/module/components/Toggle/Toggle.js +97 -0
- package/lib/module/components/index.js +16 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -0
- package/lib/module/design-tokens/figma-variables-resolver.js +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/AmountInput/AmountInput.d.ts +23 -0
- package/lib/typescript/src/components/AmountInput/index.d.ts +3 -0
- package/lib/typescript/src/components/Button/Button.d.ts +6 -1
- package/lib/typescript/src/components/CardProviderInfo/CardProviderInfo.d.ts +24 -0
- package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +30 -0
- package/lib/typescript/src/components/EmptyState/EmptyState.d.ts +6 -1
- package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +18 -26
- package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +23 -0
- package/lib/typescript/src/components/NoteInput/index.d.ts +3 -0
- package/lib/typescript/src/components/Numpad/Numpad.d.ts +35 -0
- package/lib/typescript/src/components/OTP/OTP.d.ts +36 -0
- package/lib/typescript/src/components/PortfolioHero/PortfolioHero.d.ts +21 -0
- package/lib/typescript/src/components/ProductLabel/ProductLabel.d.ts +14 -0
- package/lib/typescript/src/components/ProgressBadge/ProgressBadge.d.ts +36 -0
- package/lib/typescript/src/components/ProgressBadge/index.d.ts +3 -0
- package/lib/typescript/src/components/StatItem/StatItem.d.ts +21 -0
- package/lib/typescript/src/components/StatusHero/StatusHero.d.ts +47 -0
- package/lib/typescript/src/components/SwappableAmount/SwappableAmount.d.ts +22 -0
- package/lib/typescript/src/components/Tabs/TabItem.d.ts +29 -0
- package/lib/typescript/src/components/Tabs/Tabs.d.ts +44 -0
- package/lib/typescript/src/components/Text/Text.d.ts +14 -0
- package/lib/typescript/src/components/Toggle/Toggle.d.ts +29 -0
- package/lib/typescript/src/components/index.d.ts +15 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AmountInput/AmountInput.tsx +81 -0
- package/src/components/AmountInput/index.ts +2 -0
- package/src/components/Button/Button.tsx +40 -20
- package/src/components/CardProviderInfo/CardProviderInfo.tsx +81 -0
- package/src/components/Checkbox/Checkbox.tsx +238 -0
- package/src/components/EmptyState/EmptyState.tsx +7 -1
- package/src/components/MoneyValue/MoneyValue.tsx +134 -79
- package/src/components/NoteInput/NoteInput.tsx +146 -0
- package/src/components/NoteInput/index.ts +2 -0
- package/src/components/Numpad/Numpad.tsx +162 -0
- package/src/components/OTP/OTP.tsx +275 -0
- package/src/components/PortfolioHero/PortfolioHero.tsx +91 -0
- package/src/components/ProductLabel/ProductLabel.tsx +58 -0
- package/src/components/ProgressBadge/ProgressBadge.tsx +172 -0
- package/src/components/ProgressBadge/index.ts +2 -0
- package/src/components/StatItem/StatItem.tsx +71 -0
- package/src/components/StatusHero/StatusHero.tsx +156 -0
- package/src/components/SwappableAmount/SwappableAmount.tsx +92 -0
- package/src/components/Tabs/TabItem.tsx +96 -0
- package/src/components/Tabs/Tabs.tsx +105 -0
- package/src/components/Text/Text.tsx +48 -0
- package/src/components/Toggle/Toggle.tsx +122 -0
- package/src/components/index.ts +15 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -0
- package/src/design-tokens/figma-variables-resolver.ts +1 -1
- package/src/icons/registry.ts +1 -1
- package/lib/commonjs/design-tokens/JFS Variables-variables-full.json +0 -1
- package/lib/module/design-tokens/JFS Variables-variables-full.json +0 -1
- package/src/design-tokens/JFS Variables-variables-full.json +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput as RNTextInput,
|
|
6
|
+
Pressable,
|
|
7
|
+
Animated,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
13
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
14
|
+
import SupportText, { type SupportTextProps } from '../SupportText/SupportText'
|
|
15
|
+
import { cloneChildrenWithModes } from '../../utils/react-utils'
|
|
16
|
+
|
|
17
|
+
export type OTPProps = {
|
|
18
|
+
/** Number of OTP digits. Defaults to 6. */
|
|
19
|
+
length?: number
|
|
20
|
+
/** Controlled value of the OTP input. */
|
|
21
|
+
value?: string
|
|
22
|
+
/** Default value for uncontrolled usage. */
|
|
23
|
+
defaultValue?: string
|
|
24
|
+
/** Called when the OTP value changes. Receives the current string of entered digits. */
|
|
25
|
+
onChange?: (value: string) => void
|
|
26
|
+
/** Alias for onChange — called when the OTP value changes. */
|
|
27
|
+
onValueChange?: (value: string) => void
|
|
28
|
+
/** Called when all digits have been entered. Receives the complete OTP string. */
|
|
29
|
+
onComplete?: (value: string) => void
|
|
30
|
+
/** Whether the OTP input is disabled. */
|
|
31
|
+
isDisabled?: boolean
|
|
32
|
+
/** Whether the OTP input is in an invalid/error state. */
|
|
33
|
+
isInvalid?: boolean
|
|
34
|
+
/** Regex pattern to filter allowed characters. Defaults to digits only. */
|
|
35
|
+
allowedPattern?: RegExp
|
|
36
|
+
/** Auto-focus the input on mount. */
|
|
37
|
+
autoFocus?: boolean
|
|
38
|
+
/** Design token modes for Figma token resolution. */
|
|
39
|
+
modes?: Record<string, any>
|
|
40
|
+
/** Container style override. */
|
|
41
|
+
style?: StyleProp<ViewStyle>
|
|
42
|
+
/** Optional SupportText rendered below the slots. Pass a string for the label or a ReactNode for full control. */
|
|
43
|
+
supportText?: React.ReactNode
|
|
44
|
+
/** SupportText status when using the string shorthand. */
|
|
45
|
+
supportTextStatus?: SupportTextProps['status']
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DIGITS_ONLY = /^\d*$/
|
|
49
|
+
|
|
50
|
+
function OTP({
|
|
51
|
+
length = 6,
|
|
52
|
+
value: controlledValue,
|
|
53
|
+
defaultValue = '',
|
|
54
|
+
onChange,
|
|
55
|
+
onValueChange,
|
|
56
|
+
onComplete,
|
|
57
|
+
isDisabled = false,
|
|
58
|
+
isInvalid = false,
|
|
59
|
+
allowedPattern = DIGITS_ONLY,
|
|
60
|
+
autoFocus = false,
|
|
61
|
+
modes: propModes = {},
|
|
62
|
+
style,
|
|
63
|
+
supportText,
|
|
64
|
+
supportTextStatus,
|
|
65
|
+
}: OTPProps) {
|
|
66
|
+
const { modes: globalModes } = useTokens()
|
|
67
|
+
const modes = { ...globalModes, ...propModes }
|
|
68
|
+
|
|
69
|
+
const isControlled = controlledValue !== undefined
|
|
70
|
+
const [internalValue, setInternalValue] = useState(defaultValue)
|
|
71
|
+
const currentValue = isControlled ? controlledValue : internalValue
|
|
72
|
+
|
|
73
|
+
const inputRef = useRef<RNTextInput>(null)
|
|
74
|
+
const [isFocused, setIsFocused] = useState(false)
|
|
75
|
+
|
|
76
|
+
const caretAnim = useRef(new Animated.Value(1)).current
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!isFocused) return
|
|
80
|
+
const blink = Animated.loop(
|
|
81
|
+
Animated.sequence([
|
|
82
|
+
Animated.timing(caretAnim, { toValue: 0, duration: 400, useNativeDriver: true }),
|
|
83
|
+
Animated.timing(caretAnim, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
84
|
+
]),
|
|
85
|
+
)
|
|
86
|
+
blink.start()
|
|
87
|
+
return () => blink.stop()
|
|
88
|
+
}, [isFocused, caretAnim])
|
|
89
|
+
|
|
90
|
+
const prevCompleteRef = useRef(false)
|
|
91
|
+
|
|
92
|
+
const setValue = useCallback(
|
|
93
|
+
(next: string) => {
|
|
94
|
+
const clamped = next.slice(0, length)
|
|
95
|
+
if (!isControlled) setInternalValue(clamped)
|
|
96
|
+
onChange?.(clamped)
|
|
97
|
+
onValueChange?.(clamped)
|
|
98
|
+
|
|
99
|
+
if (clamped.length === length && !prevCompleteRef.current) {
|
|
100
|
+
prevCompleteRef.current = true
|
|
101
|
+
onComplete?.(clamped)
|
|
102
|
+
}
|
|
103
|
+
if (clamped.length < length) {
|
|
104
|
+
prevCompleteRef.current = false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[length, isControlled, onChange, onValueChange, onComplete],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const handleChangeText = useCallback(
|
|
111
|
+
(text: string) => {
|
|
112
|
+
if (isDisabled) return
|
|
113
|
+
if (!allowedPattern.test(text)) return
|
|
114
|
+
setValue(text)
|
|
115
|
+
},
|
|
116
|
+
[isDisabled, allowedPattern, setValue],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const handlePress = useCallback(() => {
|
|
120
|
+
if (isDisabled) return
|
|
121
|
+
inputRef.current?.focus()
|
|
122
|
+
}, [isDisabled])
|
|
123
|
+
|
|
124
|
+
// --- Token resolution (state-independent tokens resolved once) ---
|
|
125
|
+
const otpGap = Number(getVariableByName('otp/gap', modes)) || 36
|
|
126
|
+
const otpPaddingH = Number(getVariableByName('otp/padding/horizontal', modes)) || 8
|
|
127
|
+
const otpPaddingV = Number(getVariableByName('otp/padding/vertical', modes)) || 8
|
|
128
|
+
|
|
129
|
+
const slotWidth = Number(getVariableByName('pinSlot/width', modes)) || 48
|
|
130
|
+
const slotGap = Number(getVariableByName('pinSlot/gap', modes)) || 8
|
|
131
|
+
// digit/color has no state variants in Figma — resolved once from the Output collection
|
|
132
|
+
const digitColor = (getVariableByName('pinSlot/digit/color', modes) as string) || '#000000'
|
|
133
|
+
const digitFontSize = Number(getVariableByName('pinSlot/digit/fontSize', modes)) || 24
|
|
134
|
+
const digitFontFamily = (getVariableByName('pinSlot/digit/fontFamily', modes) as string) || 'JioType Var'
|
|
135
|
+
const digitLineHeight = Number(getVariableByName('pinSlot/digit/lineHeight', modes)) || 29
|
|
136
|
+
const digitFontWeight = (getVariableByName('pinSlot/digit/fontWeight', modes) as string) || '500'
|
|
137
|
+
const underlineHeight = Number(getVariableByName('pinSlot/underline/height', modes)) || 2
|
|
138
|
+
const underlineRadius = Number(getVariableByName('pinSlot/underline/radius', modes)) || 1
|
|
139
|
+
|
|
140
|
+
// --- State-driven slot modes ---
|
|
141
|
+
// Collection name in Figma is "Input/PINSlot States" (double space before States).
|
|
142
|
+
// Only PinSlot/underline/color (capital P/S) lives in this collection with Idle/Active/Error modes.
|
|
143
|
+
// isInvalid takes priority over active focus; the component maps semantic state → token mode
|
|
144
|
+
// internally so consumers never need to know the collection key name.
|
|
145
|
+
const getSlotModes = (isActiveSlot: boolean): Record<string, any> => {
|
|
146
|
+
if (isInvalid) return { ...modes, 'Input/PINSlot States': 'Error' }
|
|
147
|
+
if (isActiveSlot && isFocused) return { ...modes, 'Input/PINSlot States': 'Active' }
|
|
148
|
+
return { ...modes, 'Input/PINSlot States': 'Idle' }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Styles ---
|
|
152
|
+
const containerStyle: ViewStyle = {
|
|
153
|
+
flexDirection: 'column',
|
|
154
|
+
alignItems: 'flex-end',
|
|
155
|
+
gap: otpGap,
|
|
156
|
+
paddingHorizontal: otpPaddingH,
|
|
157
|
+
paddingVertical: otpPaddingV,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const slotWrapStyle: ViewStyle = {
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
gap: 8,
|
|
163
|
+
alignItems: 'flex-start',
|
|
164
|
+
alignSelf: 'stretch',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const renderSlot = (index: number) => {
|
|
168
|
+
const char = currentValue[index]
|
|
169
|
+
const isActiveSlot = index === currentValue.length && currentValue.length < length
|
|
170
|
+
const isFilled = char !== undefined
|
|
171
|
+
|
|
172
|
+
// Underline color is the only state-sensitive token (lives in "Input/PINSlot States" collection).
|
|
173
|
+
// Note: token name is "PinSlot/underline/color" (capital P/S) — different from the static
|
|
174
|
+
// "pinSlot/underline/color" in the Output collection.
|
|
175
|
+
const slotModes = getSlotModes(isActiveSlot)
|
|
176
|
+
const underlineColor = (getVariableByName('PinSlot/underline/color', slotModes) as string) || '#303338'
|
|
177
|
+
|
|
178
|
+
const slotStyle: ViewStyle = {
|
|
179
|
+
width: slotWidth,
|
|
180
|
+
flexDirection: 'column',
|
|
181
|
+
alignItems: 'center',
|
|
182
|
+
justifyContent: 'flex-end',
|
|
183
|
+
gap: slotGap,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const digitStyle: TextStyle = {
|
|
187
|
+
fontFamily: digitFontFamily,
|
|
188
|
+
fontWeight: digitFontWeight as TextStyle['fontWeight'],
|
|
189
|
+
fontSize: digitFontSize,
|
|
190
|
+
lineHeight: digitLineHeight,
|
|
191
|
+
color: digitColor,
|
|
192
|
+
textAlign: 'center',
|
|
193
|
+
minWidth: '100%' as any,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const underlineStyle: ViewStyle = {
|
|
197
|
+
width: slotWidth,
|
|
198
|
+
height: underlineHeight,
|
|
199
|
+
borderRadius: underlineRadius,
|
|
200
|
+
backgroundColor: underlineColor,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<View key={index} style={slotStyle}>
|
|
205
|
+
<View style={{ minHeight: digitLineHeight, justifyContent: 'center', alignItems: 'center', width: '100%' }}>
|
|
206
|
+
{isFilled ? (
|
|
207
|
+
<Text style={digitStyle}>{char}</Text>
|
|
208
|
+
) : isActiveSlot && isFocused ? (
|
|
209
|
+
<Animated.View
|
|
210
|
+
style={{
|
|
211
|
+
width: 2,
|
|
212
|
+
height: digitFontSize,
|
|
213
|
+
backgroundColor: digitColor,
|
|
214
|
+
opacity: caretAnim,
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
) : (
|
|
218
|
+
<Text style={[digitStyle, { color: 'transparent' }]}>{'\u00A0'}</Text>
|
|
219
|
+
)}
|
|
220
|
+
</View>
|
|
221
|
+
<View style={underlineStyle} />
|
|
222
|
+
</View>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const renderSupportText = () => {
|
|
227
|
+
if (!supportText) return null
|
|
228
|
+
if (typeof supportText === 'string') {
|
|
229
|
+
return (
|
|
230
|
+
<SupportText
|
|
231
|
+
label={supportText}
|
|
232
|
+
status={supportTextStatus ?? (isInvalid ? 'Error' : 'Neutral')}
|
|
233
|
+
modes={modes}
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
return <>{cloneChildrenWithModes(React.Children.toArray(supportText), modes)}</>
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<Pressable
|
|
242
|
+
style={[containerStyle, isDisabled && { opacity: 0.4 }, style]}
|
|
243
|
+
onPress={handlePress}
|
|
244
|
+
disabled={isDisabled}
|
|
245
|
+
accessibilityRole="none"
|
|
246
|
+
>
|
|
247
|
+
<RNTextInput
|
|
248
|
+
ref={inputRef}
|
|
249
|
+
value={currentValue}
|
|
250
|
+
onChangeText={handleChangeText}
|
|
251
|
+
maxLength={length}
|
|
252
|
+
keyboardType="number-pad"
|
|
253
|
+
autoFocus={autoFocus}
|
|
254
|
+
editable={!isDisabled}
|
|
255
|
+
onFocus={() => setIsFocused(true)}
|
|
256
|
+
onBlur={() => setIsFocused(false)}
|
|
257
|
+
caretHidden
|
|
258
|
+
style={{
|
|
259
|
+
position: 'absolute',
|
|
260
|
+
width: 1,
|
|
261
|
+
height: 1,
|
|
262
|
+
opacity: 0,
|
|
263
|
+
}}
|
|
264
|
+
accessibilityLabel={`OTP input, ${length} digits`}
|
|
265
|
+
accessibilityHint="Enter your verification code"
|
|
266
|
+
/>
|
|
267
|
+
<View style={slotWrapStyle}>
|
|
268
|
+
{Array.from({ length }, (_, i) => renderSlot(i))}
|
|
269
|
+
</View>
|
|
270
|
+
{renderSupportText()}
|
|
271
|
+
</Pressable>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default OTP
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
type ViewStyle,
|
|
6
|
+
type TextStyle,
|
|
7
|
+
type ImageSourcePropType,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
|
+
import { cloneChildrenWithModes } from '../../utils/react-utils'
|
|
11
|
+
import Avatar from '../Avatar/Avatar'
|
|
12
|
+
import MoneyValue from '../MoneyValue/MoneyValue'
|
|
13
|
+
|
|
14
|
+
export type PortfolioHeroProps = {
|
|
15
|
+
/** Product label text displayed next to the avatar. */
|
|
16
|
+
label?: string
|
|
17
|
+
/** Image source for the product avatar. */
|
|
18
|
+
imageSource?: ImageSourcePropType | string
|
|
19
|
+
/** Monetary value to display. */
|
|
20
|
+
value?: string
|
|
21
|
+
/** Currency symbol or ISO code. */
|
|
22
|
+
currency?: string
|
|
23
|
+
/** Modes configuration mapped to Figma tokens. */
|
|
24
|
+
modes?: Record<string, any>
|
|
25
|
+
/** Slot content rendered below the money value (e.g. Badge, subtitle). */
|
|
26
|
+
children?: React.ReactNode
|
|
27
|
+
/** Container style override. */
|
|
28
|
+
style?: ViewStyle
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function PortfolioHero({
|
|
32
|
+
label = 'Gold',
|
|
33
|
+
imageSource,
|
|
34
|
+
value = '12,000.11',
|
|
35
|
+
currency = '₹',
|
|
36
|
+
modes = { Context3: 'Balance' },
|
|
37
|
+
children,
|
|
38
|
+
style,
|
|
39
|
+
}: PortfolioHeroProps) {
|
|
40
|
+
const gap = getVariableByName('portfolioHero/gap', modes) ?? 16
|
|
41
|
+
const padding = getVariableByName('portfolioHero/padding', modes) ?? 8
|
|
42
|
+
const slotWrapGap = getVariableByName('portfolioHero/slotWrap/gap', modes) ?? 8
|
|
43
|
+
|
|
44
|
+
const containerStyle: ViewStyle = {
|
|
45
|
+
flexDirection: 'column',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
gap: gap as number,
|
|
49
|
+
padding: padding as number,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const productLabelStyle: ViewStyle = {
|
|
53
|
+
flexDirection: 'row',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
gap: 8,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const labelTextStyle: TextStyle = {
|
|
59
|
+
fontFamily: 'System',
|
|
60
|
+
fontWeight: '700',
|
|
61
|
+
fontSize: 16,
|
|
62
|
+
lineHeight: 16 * 1.3,
|
|
63
|
+
color: '#0d0d0f',
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const slotWrapStyle: ViewStyle = {
|
|
68
|
+
flexDirection: 'column',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
gap: slotWrapGap as number,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View style={[containerStyle, style]}>
|
|
75
|
+
<View style={productLabelStyle}>
|
|
76
|
+
<Avatar style="Image" {...(imageSource != null && { imageSource })} modes={modes} />
|
|
77
|
+
<Text style={labelTextStyle}>{label}</Text>
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
<MoneyValue value={value} currency={currency} modes={modes} />
|
|
81
|
+
|
|
82
|
+
{children && (
|
|
83
|
+
<View style={slotWrapStyle}>
|
|
84
|
+
{cloneChildrenWithModes(children, modes)}
|
|
85
|
+
</View>
|
|
86
|
+
)}
|
|
87
|
+
</View>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default PortfolioHero
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, type ViewStyle, type TextStyle, type ImageSourcePropType } from 'react-native'
|
|
3
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
+
import Avatar from '../Avatar/Avatar'
|
|
5
|
+
|
|
6
|
+
export type ProductLabelProps = {
|
|
7
|
+
/** The product name label text. */
|
|
8
|
+
label?: string
|
|
9
|
+
/** Image source for the product avatar. */
|
|
10
|
+
imageSource?: ImageSourcePropType | string
|
|
11
|
+
/** Modes configuration for design token resolution. */
|
|
12
|
+
modes?: Record<string, any>
|
|
13
|
+
/** Container style override. */
|
|
14
|
+
style?: ViewStyle
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ProductLabel({
|
|
18
|
+
label = 'Gold',
|
|
19
|
+
imageSource,
|
|
20
|
+
modes = {},
|
|
21
|
+
style,
|
|
22
|
+
}: ProductLabelProps) {
|
|
23
|
+
const gap = getVariableByName('productLabel/gap', modes) ?? 8
|
|
24
|
+
|
|
25
|
+
const textColor = getVariableByName('productLabel/text/color', modes) ?? '#000000'
|
|
26
|
+
const textFontSize = getVariableByName('productLabel/text/fontSize', modes) ?? 16
|
|
27
|
+
const textFontFamily = getVariableByName('productLabel/text/fontFamily', modes) ?? 'JioType Var'
|
|
28
|
+
const textFontWeight = getVariableByName('productLabel/text/fontWeight', modes) ?? 700
|
|
29
|
+
const textLineHeight = getVariableByName('productLabel/text/lineHeight', modes) ?? 21
|
|
30
|
+
|
|
31
|
+
const containerStyle: ViewStyle = {
|
|
32
|
+
flexDirection: 'row',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
gap: gap as number,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const labelTextStyle: TextStyle = {
|
|
38
|
+
color: textColor as string,
|
|
39
|
+
fontSize: textFontSize as number,
|
|
40
|
+
fontFamily: textFontFamily as string,
|
|
41
|
+
fontWeight: String(textFontWeight) as TextStyle['fontWeight'],
|
|
42
|
+
lineHeight: textLineHeight as number,
|
|
43
|
+
textAlign: 'center',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View style={[containerStyle, style]}>
|
|
48
|
+
<Avatar
|
|
49
|
+
style="Image"
|
|
50
|
+
imageSource={imageSource}
|
|
51
|
+
modes={modes}
|
|
52
|
+
/>
|
|
53
|
+
<Text style={labelTextStyle}>{label}</Text>
|
|
54
|
+
</View>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default ProductLabel
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React 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 Icon from '../../icons/Icon'
|
|
11
|
+
import { cloneChildrenWithModes } from '../../utils/react-utils'
|
|
12
|
+
|
|
13
|
+
export type ProgressBadgeProps = {
|
|
14
|
+
/** The text displayed in the badge (e.g. "Live price...") */
|
|
15
|
+
taskName?: string
|
|
16
|
+
/** The icon name to display to the left of the text */
|
|
17
|
+
iconName?: string
|
|
18
|
+
/** The progress value between 0 and 100 */
|
|
19
|
+
value?: number
|
|
20
|
+
/** Modes object passed to `getVariableByName` for design token resolution */
|
|
21
|
+
modes?: Record<string, any>
|
|
22
|
+
/** Optional container style overrides */
|
|
23
|
+
style?: StyleProp<ViewStyle>
|
|
24
|
+
/** Optional text style overrides */
|
|
25
|
+
textStyle?: StyleProp<TextStyle>
|
|
26
|
+
/** Accessibility label for screen readers */
|
|
27
|
+
accessibilityLabel?: string
|
|
28
|
+
} & React.ComponentProps<typeof View>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ProgressBadge component that displays an icon, text, and an internal progress bar.
|
|
32
|
+
*
|
|
33
|
+
* All visual attributes resolve from Figma tokens via `getVariableByName` using the provided `modes`.
|
|
34
|
+
*
|
|
35
|
+
* @component
|
|
36
|
+
* @param {Object} props
|
|
37
|
+
* @param {string} [props.taskName="Live price: [price] (00:43)"] - The text displayed.
|
|
38
|
+
* @param {string} [props.iconName="ic_live"] - Icon name from the registry.
|
|
39
|
+
* @param {number} [props.value=0] - The progress value between 0 and 100.
|
|
40
|
+
* @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for design tokens.
|
|
41
|
+
* @param {Object} [props.style] - Optional container style overrides.
|
|
42
|
+
* @param {Object} [props.textStyle] - Optional text style overrides.
|
|
43
|
+
* @param {string} [props.accessibilityLabel] - Accessibility label.
|
|
44
|
+
*/
|
|
45
|
+
function ProgressBadge({
|
|
46
|
+
taskName = 'Live price: [price] (00:43)',
|
|
47
|
+
iconName = 'ic_live',
|
|
48
|
+
value = 0,
|
|
49
|
+
modes = {},
|
|
50
|
+
style,
|
|
51
|
+
textStyle: textStyleOverride,
|
|
52
|
+
accessibilityLabel,
|
|
53
|
+
...rest
|
|
54
|
+
}: ProgressBadgeProps) {
|
|
55
|
+
// Resolve layout tokens
|
|
56
|
+
const backgroundColor =
|
|
57
|
+
getVariableByName('progressBadge/background', modes) || '#ffffff'
|
|
58
|
+
const progressColor =
|
|
59
|
+
getVariableByName('progressBadge/progress/color', modes) || '#ebebed'
|
|
60
|
+
const gap = getVariableByName('progressBadge/gap', modes) ?? 12
|
|
61
|
+
const paddingHorizontal =
|
|
62
|
+
getVariableByName('progressBadge/padding/horizontal', modes) ?? 16
|
|
63
|
+
const paddingVertical =
|
|
64
|
+
getVariableByName('progressBadge/padding/vertical', modes) ?? 6
|
|
65
|
+
const borderRadius = getVariableByName('progressBadge/radius', modes) ?? 999
|
|
66
|
+
const descriptionGap =
|
|
67
|
+
getVariableByName('progressBadge/description/gap', modes) ?? 8
|
|
68
|
+
|
|
69
|
+
// Resolve typography tokens
|
|
70
|
+
const fontFamily =
|
|
71
|
+
getVariableByName('progressBadge/fontFamily', modes) || 'JioType_Var:Medium'
|
|
72
|
+
const fontWeightRaw = getVariableByName('progressBadge/fontWeight', modes) ?? 500
|
|
73
|
+
const fontWeight =
|
|
74
|
+
typeof fontWeightRaw === 'number'
|
|
75
|
+
? fontWeightRaw.toString()
|
|
76
|
+
: fontWeightRaw
|
|
77
|
+
const fontSize = getVariableByName('progressBadge/fontSize', modes) ?? 14
|
|
78
|
+
const lineHeight = getVariableByName('progressBadge/lineHeight', modes) ?? 17
|
|
79
|
+
const textColor =
|
|
80
|
+
getVariableByName('progressBadge/foreground', modes) || '#000000'
|
|
81
|
+
|
|
82
|
+
// Resolve icon tokens
|
|
83
|
+
const iconColor =
|
|
84
|
+
getVariableByName('progressBadge/icon/color', modes) || '#f50030'
|
|
85
|
+
const iconSize = getVariableByName('progressBadge/icon/size', modes) ?? 18
|
|
86
|
+
|
|
87
|
+
const containerStyle: ViewStyle = {
|
|
88
|
+
backgroundColor,
|
|
89
|
+
flexDirection: 'row',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
gap,
|
|
92
|
+
paddingHorizontal,
|
|
93
|
+
paddingVertical,
|
|
94
|
+
borderRadius,
|
|
95
|
+
overflow: 'hidden',
|
|
96
|
+
position: 'relative',
|
|
97
|
+
flexWrap: 'wrap',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const descriptionContainerStyle: ViewStyle = {
|
|
101
|
+
flexDirection: 'row',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
gap: descriptionGap,
|
|
105
|
+
flex: 1,
|
|
106
|
+
paddingVertical: 0,
|
|
107
|
+
minHeight: 1,
|
|
108
|
+
minWidth: 1,
|
|
109
|
+
position: 'relative',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const textStyle: TextStyle = {
|
|
113
|
+
fontFamily,
|
|
114
|
+
fontWeight,
|
|
115
|
+
fontSize,
|
|
116
|
+
lineHeight,
|
|
117
|
+
color: textColor,
|
|
118
|
+
textAlign: 'center',
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure value is between 0 and 100
|
|
124
|
+
const clampedValue = Math.max(0, Math.min(100, value))
|
|
125
|
+
|
|
126
|
+
const progressStyle: ViewStyle = {
|
|
127
|
+
position: 'absolute',
|
|
128
|
+
left: 0,
|
|
129
|
+
top: 0,
|
|
130
|
+
bottom: 0,
|
|
131
|
+
width: `${clampedValue}%`,
|
|
132
|
+
backgroundColor: progressColor,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const defaultAccessibilityLabel = accessibilityLabel || taskName
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View
|
|
139
|
+
style={[containerStyle, style]}
|
|
140
|
+
accessibilityRole="text"
|
|
141
|
+
accessibilityLabel={defaultAccessibilityLabel}
|
|
142
|
+
{...rest}
|
|
143
|
+
>
|
|
144
|
+
{/* Background Progress Bar */}
|
|
145
|
+
<View style={progressStyle} pointerEvents="none" />
|
|
146
|
+
|
|
147
|
+
{/* Description Header */}
|
|
148
|
+
<View style={descriptionContainerStyle}>
|
|
149
|
+
{iconName ? (
|
|
150
|
+
<Icon
|
|
151
|
+
name={iconName}
|
|
152
|
+
size={iconSize}
|
|
153
|
+
color={iconColor}
|
|
154
|
+
accessibilityElementsHidden={true}
|
|
155
|
+
importantForAccessibility="no"
|
|
156
|
+
/>
|
|
157
|
+
) : null}
|
|
158
|
+
<Text
|
|
159
|
+
style={[textStyle, textStyleOverride]}
|
|
160
|
+
numberOfLines={1}
|
|
161
|
+
ellipsizeMode="tail"
|
|
162
|
+
accessibilityElementsHidden={true}
|
|
163
|
+
importantForAccessibility="no"
|
|
164
|
+
>
|
|
165
|
+
{taskName}
|
|
166
|
+
</Text>
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default ProgressBadge
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
+
|
|
5
|
+
export type StatItemProps = {
|
|
6
|
+
/** The small descriptive label above the value. */
|
|
7
|
+
label?: string
|
|
8
|
+
/** The large prominent value to display. */
|
|
9
|
+
value?: string
|
|
10
|
+
/** Design token modes for theming. */
|
|
11
|
+
modes?: Record<string, any>
|
|
12
|
+
/** Override container styles. */
|
|
13
|
+
style?: StyleProp<ViewStyle>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* StatItem displays a label/value pair with the value in a large, bold style.
|
|
18
|
+
* Useful for metrics, product stats, or KPI callouts.
|
|
19
|
+
*
|
|
20
|
+
* @component
|
|
21
|
+
* @param {StatItemProps} props
|
|
22
|
+
*/
|
|
23
|
+
function StatItem({
|
|
24
|
+
label = 'Purity verified by NABL',
|
|
25
|
+
value = '99.99%',
|
|
26
|
+
modes = {},
|
|
27
|
+
style,
|
|
28
|
+
}: StatItemProps) {
|
|
29
|
+
const gap = getVariableByName('statItem/gap', modes) ?? 2
|
|
30
|
+
|
|
31
|
+
const labelForeground = getVariableByName('statItem/label/foreground', modes) ?? '#1a1c1f'
|
|
32
|
+
const labelFontFamily = getVariableByName('statItem/label/fontFamily', modes) ?? 'JioType Var'
|
|
33
|
+
const labelFontSize = getVariableByName('statItem/label/fontSize', modes) ?? 12
|
|
34
|
+
const labelFontWeight = getVariableByName('statItem/label/fontWeight', modes) ?? '500'
|
|
35
|
+
const labelLineHeight = getVariableByName('statItem/label/lineHeight', modes) ?? 16
|
|
36
|
+
|
|
37
|
+
const valueForeground = getVariableByName('statItem/value/foreground', modes) ?? '#0d0d0f'
|
|
38
|
+
const valueFontFamily = getVariableByName('statItem/value/fontFamily', modes) ?? 'JioType Var'
|
|
39
|
+
const valueFontSize = getVariableByName('statItem/value/fontSize', modes) ?? 26
|
|
40
|
+
const valueFontWeight = getVariableByName('statItem/value/fontWeight', modes) ?? '900'
|
|
41
|
+
const valueLineHeight = getVariableByName('statItem/value/lineHeight', modes) ?? 26
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View style={[{ gap: gap as number }, style]}>
|
|
45
|
+
<Text
|
|
46
|
+
style={{
|
|
47
|
+
color: labelForeground as string,
|
|
48
|
+
fontFamily: labelFontFamily as string,
|
|
49
|
+
fontSize: labelFontSize as number,
|
|
50
|
+
fontWeight: String(labelFontWeight) as any,
|
|
51
|
+
lineHeight: labelLineHeight as number,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{label}
|
|
55
|
+
</Text>
|
|
56
|
+
<Text
|
|
57
|
+
style={{
|
|
58
|
+
color: valueForeground as string,
|
|
59
|
+
fontFamily: valueFontFamily as string,
|
|
60
|
+
fontSize: valueFontSize as number,
|
|
61
|
+
fontWeight: String(valueFontWeight) as any,
|
|
62
|
+
lineHeight: valueLineHeight as number,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{value}
|
|
66
|
+
</Text>
|
|
67
|
+
</View>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default StatItem
|