jfs-components 0.0.43 → 0.0.44
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/Button/Button.js +15 -1
- package/lib/commonjs/components/Checkbox/Checkbox.js +208 -0
- 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/StatusHero/StatusHero.js +148 -0
- package/lib/commonjs/components/Tabs/TabItem.js +79 -0
- package/lib/commonjs/components/Tabs/Tabs.js +88 -0
- package/lib/commonjs/components/index.js +42 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Button/Button.js +14 -1
- package/lib/module/components/Checkbox/Checkbox.js +205 -0
- 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/StatusHero/StatusHero.js +142 -0
- package/lib/module/components/Tabs/TabItem.js +74 -0
- package/lib/module/components/Tabs/Tabs.js +78 -0
- package/lib/module/components/index.js +6 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Button/Button.d.ts +6 -1
- package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +30 -0
- 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/StatusHero/StatusHero.d.ts +47 -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/index.d.ts +6 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +14 -1
- package/src/components/Checkbox/Checkbox.tsx +238 -0
- 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/StatusHero/StatusHero.tsx +156 -0
- package/src/components/Tabs/TabItem.tsx +96 -0
- package/src/components/Tabs/Tabs.tsx +105 -0
- package/src/components/index.ts +6 -0
- package/src/icons/registry.ts +1 -1
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import React, { useMemo, useEffect, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
Animated,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
} from 'react-native'
|
|
3
11
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
12
|
|
|
5
13
|
// Map of common ISO 4217 currency codes to display symbols
|
|
@@ -37,50 +45,44 @@ const CURRENCY_SYMBOLS = {
|
|
|
37
45
|
NPR: 'Rs',
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
type MoneyValueProps = {
|
|
41
|
-
value
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
export type MoneyValueProps = {
|
|
49
|
+
/** Monetary value to display. */
|
|
50
|
+
value?: string | number
|
|
51
|
+
/** Currency symbol or ISO code (e.g. "INR"). */
|
|
52
|
+
currency?: string
|
|
53
|
+
/** Explicitly override negative display. If undefined, auto-detects from value. */
|
|
54
|
+
negative?: boolean
|
|
55
|
+
/** When true, masks the value for privacy (e.g. •••). */
|
|
56
|
+
hidden?: boolean
|
|
57
|
+
/** When true, a blinking vertical cursor is shown at the end of the value text. */
|
|
58
|
+
focused?: boolean
|
|
59
|
+
/** Modes configuration mapped to Figma tokens. */
|
|
60
|
+
modes?: Record<string, any>
|
|
61
|
+
style?: StyleProp<ViewStyle>
|
|
62
|
+
valueStyle?: StyleProp<TextStyle>
|
|
63
|
+
currencyStyle?: StyleProp<TextStyle>
|
|
64
|
+
negativeSignStyle?: StyleProp<TextStyle>
|
|
65
|
+
accessibilityLabel?: string
|
|
66
|
+
accessibilityHint?: string
|
|
67
|
+
} & React.ComponentProps<typeof Pressable>
|
|
52
68
|
|
|
53
69
|
/**
|
|
54
70
|
* MoneyValue component that mirrors the Figma MoneyValue design.
|
|
55
71
|
*
|
|
56
|
-
* The styling is fully resolved from Figma design tokens using `getVariableByName
|
|
57
|
-
*
|
|
72
|
+
* The styling is fully resolved from Figma design tokens using `getVariableByName`.
|
|
73
|
+
* Supports separate typography scaling for currency and value.
|
|
58
74
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Negative values are auto-detected from the value prop (e.g., -500 or "-500").
|
|
64
|
-
* The `negative` prop can be used to explicitly override this behavior.
|
|
65
|
-
*
|
|
66
|
-
* @component
|
|
67
|
-
* @param {Object} props
|
|
68
|
-
* @param {string|number} [props.value="500"] - Monetary value to display. Negative values are auto-detected.
|
|
69
|
-
* @param {string} [props.currency="₹"] - Currency symbol or ISO code (e.g. "INR").
|
|
70
|
-
* @param {boolean} [props.negative] - Explicitly override negative display. If undefined, auto-detects from value.
|
|
71
|
-
* @param {Object} [props.modes={}] - Modes object passed directly to `getVariableByName`.
|
|
72
|
-
* Example: {"MoneyValue / Theme": "Default"}
|
|
73
|
-
* @param {Object} [props.style] - Optional container style overrides.
|
|
74
|
-
* @param {Object} [props.valueStyle] - Optional value text style overrides.
|
|
75
|
-
* @param {Object} [props.currencyStyle] - Optional currency text style overrides.
|
|
76
|
-
* @param {Object} [props.negativeSignStyle] - Optional negative sign text style overrides.
|
|
77
|
-
* @param {string} [props.accessibilityLabel] - Accessibility label for screen readers. If not provided, generates from value and currency
|
|
78
|
-
* @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
|
|
75
|
+
* To make this component editable without coupling it to a Numpad component,
|
|
76
|
+
* use the `onPress` prop and manage the data state in the parent layer. When
|
|
77
|
+
* the `focused` prop is provided to this component, it will display a natural
|
|
78
|
+
* blinking text cursor.
|
|
79
79
|
*/
|
|
80
80
|
function MoneyValue({
|
|
81
81
|
value = '500',
|
|
82
82
|
currency = '₹',
|
|
83
83
|
negative,
|
|
84
|
+
focused = false,
|
|
85
|
+
hidden = false,
|
|
84
86
|
modes = {},
|
|
85
87
|
style,
|
|
86
88
|
valueStyle,
|
|
@@ -88,101 +90,154 @@ function MoneyValue({
|
|
|
88
90
|
negativeSignStyle,
|
|
89
91
|
accessibilityLabel,
|
|
90
92
|
accessibilityHint,
|
|
93
|
+
onPress,
|
|
91
94
|
...rest
|
|
92
95
|
}: MoneyValueProps) {
|
|
93
96
|
// Auto-detect negative from value and compute display value
|
|
94
|
-
const { isNegative, displayValue } =
|
|
97
|
+
const { isNegative, displayValue } = useMemo(() => {
|
|
95
98
|
const stringValue = String(value)
|
|
96
99
|
const trimmed = stringValue.trim()
|
|
97
|
-
const valueIsNegative =
|
|
98
|
-
|
|
100
|
+
const valueIsNegative =
|
|
101
|
+
trimmed.startsWith('-') || (typeof value === 'number' && value < 0)
|
|
102
|
+
|
|
99
103
|
// Strip leading minus sign for display (we show it separately)
|
|
100
104
|
const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed
|
|
101
|
-
|
|
105
|
+
|
|
102
106
|
// Use explicit negative prop if provided, otherwise use auto-detected
|
|
103
107
|
const showNegative = negative !== undefined ? negative : valueIsNegative
|
|
104
|
-
|
|
108
|
+
|
|
105
109
|
return {
|
|
106
|
-
isNegative: showNegative,
|
|
107
|
-
displayValue: absoluteValue,
|
|
110
|
+
isNegative: hidden ? false : showNegative,
|
|
111
|
+
displayValue: hidden ? '•••' : absoluteValue,
|
|
108
112
|
}
|
|
109
|
-
}, [value, negative])
|
|
113
|
+
}, [value, negative, hidden])
|
|
110
114
|
|
|
111
115
|
// Resolve typography and layout tokens from Figma
|
|
112
|
-
const textColor =
|
|
113
|
-
|
|
114
|
-
const fontSize
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
116
|
+
const textColor = getVariableByName('moneyValue/text/color', modes) || '#0f0d0a'
|
|
117
|
+
|
|
118
|
+
const currencyFontSize = getVariableByName('moneyValue/currency/fontSize', modes) || 14
|
|
119
|
+
const currencyLineHeight = getVariableByName('moneyValue/currency/lineHeight', modes) || 18
|
|
120
|
+
|
|
121
|
+
const valueFontSize = getVariableByName('moneyValue/value/fontSize', modes) || 14
|
|
122
|
+
const valueLineHeight = getVariableByName('moneyValue/value/lineHeight', modes) || 18
|
|
123
|
+
|
|
124
|
+
const fontWeightValue = getVariableByName('moneyValue/fontWeight', modes) || 500
|
|
120
125
|
const fontWeight =
|
|
121
126
|
typeof fontWeightValue === 'number'
|
|
122
127
|
? fontWeightValue.toString()
|
|
123
128
|
: fontWeightValue
|
|
124
|
-
const fontFamily =
|
|
125
|
-
getVariableByName('moneyValue/supportText/fontFamily', modes) ||
|
|
126
|
-
'System'
|
|
129
|
+
const fontFamily = getVariableByName('moneyValue/fontFamily', modes) || 'System'
|
|
127
130
|
const gap = getVariableByName('moneyValue/gap', modes) || 4
|
|
128
131
|
|
|
129
132
|
// Resolve currency to a symbol, supporting both symbols and ISO codes
|
|
130
|
-
const resolvedCurrency =
|
|
133
|
+
const resolvedCurrency = useMemo(() => {
|
|
131
134
|
if (!currency) return ''
|
|
132
135
|
const upper = currency.toUpperCase ? currency.toUpperCase() : currency
|
|
133
|
-
return
|
|
136
|
+
return upper in CURRENCY_SYMBOLS
|
|
137
|
+
? CURRENCY_SYMBOLS[upper as keyof typeof CURRENCY_SYMBOLS]
|
|
138
|
+
: currency
|
|
134
139
|
}, [currency])
|
|
135
140
|
|
|
136
|
-
const
|
|
141
|
+
const currencyTextStyle: TextStyle = {
|
|
142
|
+
color: textColor,
|
|
143
|
+
fontSize: currencyFontSize as number,
|
|
144
|
+
lineHeight: currencyLineHeight as number,
|
|
145
|
+
fontWeight: fontWeight as TextStyle['fontWeight'],
|
|
146
|
+
fontFamily: fontFamily as string,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const valueTextStyle: TextStyle = {
|
|
137
150
|
color: textColor,
|
|
138
|
-
fontSize,
|
|
139
|
-
lineHeight,
|
|
140
|
-
fontWeight,
|
|
141
|
-
fontFamily,
|
|
151
|
+
fontSize: valueFontSize as number,
|
|
152
|
+
lineHeight: valueLineHeight as number,
|
|
153
|
+
fontWeight: fontWeight as TextStyle['fontWeight'],
|
|
154
|
+
fontFamily: fontFamily as string,
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
const containerStyle: ViewStyle = {
|
|
145
158
|
flexDirection: 'row',
|
|
146
159
|
alignItems: 'center',
|
|
147
|
-
gap,
|
|
160
|
+
gap: gap as number,
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
163
|
+
// Blinking cursor animation
|
|
164
|
+
const cursorOpacity = useRef(new Animated.Value(0)).current
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (focused) {
|
|
168
|
+
const animation = Animated.loop(
|
|
169
|
+
Animated.sequence([
|
|
170
|
+
Animated.timing(cursorOpacity, {
|
|
171
|
+
toValue: 1,
|
|
172
|
+
duration: 400,
|
|
173
|
+
useNativeDriver: true,
|
|
174
|
+
}),
|
|
175
|
+
Animated.timing(cursorOpacity, {
|
|
176
|
+
toValue: 0,
|
|
177
|
+
duration: 400,
|
|
178
|
+
useNativeDriver: true,
|
|
179
|
+
}),
|
|
180
|
+
])
|
|
181
|
+
)
|
|
182
|
+
animation.start()
|
|
183
|
+
return () => {
|
|
184
|
+
animation.stop()
|
|
185
|
+
cursorOpacity.setValue(0)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
cursorOpacity.setValue(0)
|
|
189
|
+
}
|
|
190
|
+
}, [focused, cursorOpacity])
|
|
153
191
|
|
|
154
192
|
return (
|
|
155
|
-
<
|
|
193
|
+
<Pressable
|
|
156
194
|
style={[containerStyle, style]}
|
|
157
195
|
accessibilityRole="text"
|
|
158
196
|
accessibilityLabel={undefined}
|
|
159
197
|
accessibilityHint={accessibilityHint}
|
|
198
|
+
onPress={onPress}
|
|
199
|
+
disabled={!onPress}
|
|
160
200
|
{...rest}
|
|
161
201
|
>
|
|
162
202
|
{isNegative && (
|
|
163
|
-
<Text
|
|
164
|
-
style={[
|
|
203
|
+
<Text
|
|
204
|
+
style={[currencyTextStyle, negativeSignStyle]}
|
|
165
205
|
accessibilityElementsHidden={true}
|
|
166
206
|
importantForAccessibility="no"
|
|
167
207
|
>
|
|
168
208
|
-
|
|
169
209
|
</Text>
|
|
170
210
|
)}
|
|
171
|
-
<Text
|
|
172
|
-
style={[
|
|
211
|
+
<Text
|
|
212
|
+
style={[currencyTextStyle, currencyStyle]}
|
|
173
213
|
accessibilityElementsHidden={true}
|
|
174
214
|
importantForAccessibility="no"
|
|
175
215
|
>
|
|
176
216
|
{resolvedCurrency}
|
|
177
217
|
</Text>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
|
|
219
|
+
{/* Group value and cursor in their own view to bypass the parent's generic gap constraint */}
|
|
220
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
221
|
+
<Text
|
|
222
|
+
style={[valueTextStyle, valueStyle]}
|
|
223
|
+
accessibilityElementsHidden={true}
|
|
224
|
+
importantForAccessibility="no"
|
|
225
|
+
>
|
|
226
|
+
{displayValue}
|
|
227
|
+
</Text>
|
|
228
|
+
{focused && (
|
|
229
|
+
<Animated.View
|
|
230
|
+
style={{
|
|
231
|
+
opacity: cursorOpacity,
|
|
232
|
+
width: 2,
|
|
233
|
+
height: (valueFontSize as number) * 1.1,
|
|
234
|
+
backgroundColor: textColor,
|
|
235
|
+
marginLeft: 2,
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
</View>
|
|
240
|
+
</Pressable>
|
|
186
241
|
)
|
|
187
242
|
}
|
|
188
243
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TextInput as RNTextInput,
|
|
5
|
+
Text,
|
|
6
|
+
Animated,
|
|
7
|
+
Pressable,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
type TextStyle,
|
|
11
|
+
type TextInputProps,
|
|
12
|
+
} from 'react-native'
|
|
13
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
14
|
+
|
|
15
|
+
export type NoteInputProps = {
|
|
16
|
+
/** The value of the text input */
|
|
17
|
+
value?: string
|
|
18
|
+
/** The placeholder when empty ("Add note" by default) */
|
|
19
|
+
placeholder?: string
|
|
20
|
+
/** Callback when text changes */
|
|
21
|
+
onChangeText?: (text: string) => void
|
|
22
|
+
/** Design token modes (e.g., {'InputState': 'Idle', 'Color Mode': 'Light'}) */
|
|
23
|
+
modes?: Record<string, any>
|
|
24
|
+
/** Custom container style */
|
|
25
|
+
style?: StyleProp<ViewStyle>
|
|
26
|
+
/** Custom text input style */
|
|
27
|
+
textStyle?: StyleProp<TextStyle>
|
|
28
|
+
/** State of the component */
|
|
29
|
+
state?: 'Editing' | 'Idle'
|
|
30
|
+
} & Omit<TextInputProps, 'style' | 'value' | 'onChangeText' | 'placeholder'>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* NoteInput component representing an interactive "Add note" badge style field.
|
|
34
|
+
* Allows the user to click, clears the placeholder text, and shows a blinking cursor when focused.
|
|
35
|
+
*/
|
|
36
|
+
export default function NoteInput({
|
|
37
|
+
value = '',
|
|
38
|
+
placeholder = 'Add note',
|
|
39
|
+
onChangeText,
|
|
40
|
+
modes = {},
|
|
41
|
+
style,
|
|
42
|
+
textStyle,
|
|
43
|
+
state: explicitState,
|
|
44
|
+
onFocus,
|
|
45
|
+
onBlur,
|
|
46
|
+
...rest
|
|
47
|
+
}: NoteInputProps) {
|
|
48
|
+
const [internalFocused, setInternalFocused] = useState(false)
|
|
49
|
+
const inputRef = useRef<RNTextInput>(null)
|
|
50
|
+
|
|
51
|
+
// Resolve tokens from Figma Design
|
|
52
|
+
const foreground = getVariableByName('noteInput/foreground', modes) || '#0d0d0f'
|
|
53
|
+
const fontSize = getVariableByName('noteInput/fontSize', modes) || 14
|
|
54
|
+
const fontFamily = getVariableByName('noteInput/fontFamily', modes) || 'JioType Var'
|
|
55
|
+
const lineHeight = getVariableByName('noteInput/lineHeight', modes) || 16
|
|
56
|
+
const fontWeightRaw = getVariableByName('noteInput/fontWeight', modes) || 700
|
|
57
|
+
const fontWeight = typeof fontWeightRaw === 'number' ? fontWeightRaw.toString() as TextStyle['fontWeight'] : fontWeightRaw as TextStyle['fontWeight']
|
|
58
|
+
|
|
59
|
+
const gap = getVariableByName('noteInput/gap', modes) || 0 // 4 in some files, 0 in context
|
|
60
|
+
const paddingHorizontal = getVariableByName('noteInput/padding/horizontal', modes) || 17
|
|
61
|
+
const paddingVertical = getVariableByName('noteInput/padding/vertical', modes) || 9
|
|
62
|
+
const radius = getVariableByName('noteInput/radius', modes) || 999
|
|
63
|
+
const borderSize = getVariableByName('noteInput/border/size', modes) || 1
|
|
64
|
+
const background = getVariableByName('noteInput/background', modes) || '#ebebed'
|
|
65
|
+
const borderColor = getVariableByName('noteInput/border/color', modes) || 'rgba(255,255,255,0)'
|
|
66
|
+
|
|
67
|
+
const containerStyle: ViewStyle = {
|
|
68
|
+
flexDirection: 'row',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
paddingHorizontal: paddingHorizontal as number,
|
|
72
|
+
paddingVertical: paddingVertical as number,
|
|
73
|
+
borderRadius: radius as number,
|
|
74
|
+
backgroundColor: background,
|
|
75
|
+
borderWidth: borderSize as number,
|
|
76
|
+
borderColor: borderColor,
|
|
77
|
+
gap: gap as number,
|
|
78
|
+
// Add specific width when editing if requested by Figma design logic, though flex fits content generically
|
|
79
|
+
alignSelf: 'flex-start',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseTextStyle: TextStyle = {
|
|
83
|
+
color: foreground,
|
|
84
|
+
fontSize: fontSize as number,
|
|
85
|
+
fontFamily: fontFamily as string,
|
|
86
|
+
lineHeight: lineHeight as number,
|
|
87
|
+
fontWeight: fontWeight,
|
|
88
|
+
padding: 0,
|
|
89
|
+
margin: 0,
|
|
90
|
+
minHeight: lineHeight as number,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleFocus = (e: any) => {
|
|
94
|
+
setInternalFocused(true)
|
|
95
|
+
if (onFocus) onFocus(e)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleBlur = (e: any) => {
|
|
99
|
+
setInternalFocused(false)
|
|
100
|
+
if (onBlur) onBlur(e)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const handlePress = () => {
|
|
104
|
+
inputRef.current?.focus()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Blinking cursor setup for custom UI representation if we were drawing it natively.
|
|
108
|
+
// We use RNTextInput's native cursor, but we can style it or ensure it's visible.
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Pressable style={[containerStyle, style]} onPress={handlePress}>
|
|
112
|
+
<View style={{ position: 'relative', justifyContent: 'center' }}>
|
|
113
|
+
<Text
|
|
114
|
+
style={[baseTextStyle, textStyle, { opacity: 0 }]}
|
|
115
|
+
accessibilityElementsHidden
|
|
116
|
+
importantForAccessibility="no"
|
|
117
|
+
>
|
|
118
|
+
{internalFocused ? (value || ' ') : (value || placeholder)}
|
|
119
|
+
</Text>
|
|
120
|
+
<RNTextInput
|
|
121
|
+
ref={inputRef}
|
|
122
|
+
value={value}
|
|
123
|
+
onChangeText={onChangeText}
|
|
124
|
+
placeholder={internalFocused ? '' : placeholder}
|
|
125
|
+
placeholderTextColor={foreground}
|
|
126
|
+
onFocus={handleFocus}
|
|
127
|
+
onBlur={handleBlur}
|
|
128
|
+
selectionColor={foreground}
|
|
129
|
+
style={[
|
|
130
|
+
baseTextStyle,
|
|
131
|
+
{
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
left: 0,
|
|
134
|
+
right: 0,
|
|
135
|
+
top: 0,
|
|
136
|
+
bottom: 0,
|
|
137
|
+
outlineStyle: 'none' as any,
|
|
138
|
+
},
|
|
139
|
+
textStyle,
|
|
140
|
+
]}
|
|
141
|
+
{...rest}
|
|
142
|
+
/>
|
|
143
|
+
</View>
|
|
144
|
+
</Pressable>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useMemo, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
type TextStyle,
|
|
9
|
+
} from 'react-native'
|
|
10
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
11
|
+
import { IconDeletebackspace } from '../../icons/components/IconDeletebackspace'
|
|
12
|
+
|
|
13
|
+
export type NumpadKeyValue =
|
|
14
|
+
| '0'
|
|
15
|
+
| '1'
|
|
16
|
+
| '2'
|
|
17
|
+
| '3'
|
|
18
|
+
| '4'
|
|
19
|
+
| '5'
|
|
20
|
+
| '6'
|
|
21
|
+
| '7'
|
|
22
|
+
| '8'
|
|
23
|
+
| '9'
|
|
24
|
+
| '.'
|
|
25
|
+
| 'backspace'
|
|
26
|
+
|
|
27
|
+
export interface NumpadProps {
|
|
28
|
+
/** Design token modes for theming (e.g., {"Color Mode": "Light"}) */
|
|
29
|
+
modes?: Record<string, any>
|
|
30
|
+
/** Callback fired when any key is pressed */
|
|
31
|
+
onKeyPress?: (key: NumpadKeyValue) => void
|
|
32
|
+
/** Whether to show the decimal point key (default: true) */
|
|
33
|
+
showDecimal?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* When true, digit positions (0-9) are randomised on each mount for
|
|
36
|
+
* anti-keylogging / shoulder-surfing protection. The decimal and
|
|
37
|
+
* backspace keys keep their fixed positions.
|
|
38
|
+
*/
|
|
39
|
+
shuffle?: boolean
|
|
40
|
+
/** Override container styles */
|
|
41
|
+
style?: StyleProp<ViewStyle>
|
|
42
|
+
/** Override individual key styles */
|
|
43
|
+
keyStyle?: StyleProp<ViewStyle>
|
|
44
|
+
/** Override text styles on digit / decimal keys */
|
|
45
|
+
keyTextStyle?: StyleProp<TextStyle>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shuffleArray<T>(arr: T[]): T[] {
|
|
49
|
+
const shuffled = [...arr]
|
|
50
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
51
|
+
const j = Math.floor(Math.random() * (i + 1))
|
|
52
|
+
;[shuffled[i]!, shuffled[j]!] = [shuffled[j]!, shuffled[i]!]
|
|
53
|
+
}
|
|
54
|
+
return shuffled
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Secure numpad component for the JFS finance system.
|
|
59
|
+
*
|
|
60
|
+
* Renders a 3×4 grid of digit keys (0-9), an optional decimal key, and a
|
|
61
|
+
* backspace key. Digit positions are shuffled by default to guard against
|
|
62
|
+
* keylogging and shoulder-surfing attacks on mobile devices.
|
|
63
|
+
*
|
|
64
|
+
* @component
|
|
65
|
+
* @param {NumpadProps} props
|
|
66
|
+
*/
|
|
67
|
+
function Numpad({
|
|
68
|
+
modes = {},
|
|
69
|
+
onKeyPress,
|
|
70
|
+
showDecimal = true,
|
|
71
|
+
shuffle = true,
|
|
72
|
+
style,
|
|
73
|
+
keyStyle,
|
|
74
|
+
keyTextStyle,
|
|
75
|
+
}: NumpadProps) {
|
|
76
|
+
const foreground = getVariableByName('numpad/foreground', modes) ?? '#141414'
|
|
77
|
+
const lineHeight = getVariableByName('numpad/lineHeight', modes) ?? 32
|
|
78
|
+
const fontFamily = getVariableByName('numpad/fontFamily', modes) ?? 'JioType Var'
|
|
79
|
+
const fontSize = getVariableByName('numpad/fontSize', modes) ?? 32
|
|
80
|
+
const rowGap = getVariableByName('numpad/gridRowGap/vertical', modes) ?? 12
|
|
81
|
+
const columnGap = getVariableByName('numpad/gridColumnGap/horizontal', modes) ?? 12
|
|
82
|
+
|
|
83
|
+
const digits = useMemo(() => {
|
|
84
|
+
const base = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
|
|
85
|
+
return shuffle ? shuffleArray(base) : base
|
|
86
|
+
}, [shuffle])
|
|
87
|
+
|
|
88
|
+
const rows: (string | null)[][] = useMemo(
|
|
89
|
+
() => [
|
|
90
|
+
digits.slice(0, 3),
|
|
91
|
+
digits.slice(3, 6),
|
|
92
|
+
digits.slice(6, 9),
|
|
93
|
+
[showDecimal ? '.' : null, digits[9]!, 'backspace'],
|
|
94
|
+
],
|
|
95
|
+
[digits, showDecimal],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const handlePress = useCallback(
|
|
99
|
+
(key: NumpadKeyValue) => {
|
|
100
|
+
onKeyPress?.(key)
|
|
101
|
+
},
|
|
102
|
+
[onKeyPress],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const textStyle: TextStyle = {
|
|
106
|
+
color: foreground as string,
|
|
107
|
+
fontFamily: fontFamily as string,
|
|
108
|
+
fontSize: fontSize as number,
|
|
109
|
+
lineHeight: lineHeight as number,
|
|
110
|
+
textAlign: 'center',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<View
|
|
115
|
+
style={[{ gap: rowGap as number }, style]}
|
|
116
|
+
accessibilityRole="none"
|
|
117
|
+
>
|
|
118
|
+
{rows.map((row, rowIndex) => (
|
|
119
|
+
<View key={rowIndex} style={{ flexDirection: 'row', gap: columnGap as number }}>
|
|
120
|
+
{row.map((key, colIndex) => {
|
|
121
|
+
if (key === null) {
|
|
122
|
+
return <View key={`empty-${colIndex}`} style={{ flex: 1 }} />
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isBackspace = key === 'backspace'
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Pressable
|
|
129
|
+
key={`${key}-${colIndex}`}
|
|
130
|
+
style={({ pressed }) => [
|
|
131
|
+
{
|
|
132
|
+
flex: 1,
|
|
133
|
+
justifyContent: 'center',
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
minHeight: 46,
|
|
136
|
+
},
|
|
137
|
+
pressed && { opacity: 0.4 },
|
|
138
|
+
keyStyle,
|
|
139
|
+
]}
|
|
140
|
+
onPress={() => handlePress(key as NumpadKeyValue)}
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
accessibilityLabel={isBackspace ? 'Backspace' : key}
|
|
143
|
+
>
|
|
144
|
+
{isBackspace ? (
|
|
145
|
+
<IconDeletebackspace
|
|
146
|
+
width={fontSize as number}
|
|
147
|
+
height={fontSize as number}
|
|
148
|
+
fill={foreground as string}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<Text style={[textStyle, keyTextStyle]}>{key}</Text>
|
|
152
|
+
)}
|
|
153
|
+
</Pressable>
|
|
154
|
+
)
|
|
155
|
+
})}
|
|
156
|
+
</View>
|
|
157
|
+
))}
|
|
158
|
+
</View>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default Numpad
|