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.
Files changed (45) hide show
  1. package/lib/commonjs/components/Button/Button.js +15 -1
  2. package/lib/commonjs/components/Checkbox/Checkbox.js +208 -0
  3. package/lib/commonjs/components/MoneyValue/MoneyValue.js +81 -49
  4. package/lib/commonjs/components/NoteInput/NoteInput.js +120 -0
  5. package/lib/commonjs/components/NoteInput/index.js +13 -0
  6. package/lib/commonjs/components/Numpad/Numpad.js +108 -0
  7. package/lib/commonjs/components/StatusHero/StatusHero.js +148 -0
  8. package/lib/commonjs/components/Tabs/TabItem.js +79 -0
  9. package/lib/commonjs/components/Tabs/Tabs.js +88 -0
  10. package/lib/commonjs/components/index.js +42 -0
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/module/components/Button/Button.js +14 -1
  13. package/lib/module/components/Checkbox/Checkbox.js +205 -0
  14. package/lib/module/components/MoneyValue/MoneyValue.js +81 -49
  15. package/lib/module/components/NoteInput/NoteInput.js +115 -0
  16. package/lib/module/components/NoteInput/index.js +3 -0
  17. package/lib/module/components/Numpad/Numpad.js +103 -0
  18. package/lib/module/components/StatusHero/StatusHero.js +142 -0
  19. package/lib/module/components/Tabs/TabItem.js +74 -0
  20. package/lib/module/components/Tabs/Tabs.js +78 -0
  21. package/lib/module/components/index.js +6 -0
  22. package/lib/module/icons/registry.js +1 -1
  23. package/lib/typescript/src/components/Button/Button.d.ts +6 -1
  24. package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +30 -0
  25. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +18 -26
  26. package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +23 -0
  27. package/lib/typescript/src/components/NoteInput/index.d.ts +3 -0
  28. package/lib/typescript/src/components/Numpad/Numpad.d.ts +35 -0
  29. package/lib/typescript/src/components/StatusHero/StatusHero.d.ts +47 -0
  30. package/lib/typescript/src/components/Tabs/TabItem.d.ts +29 -0
  31. package/lib/typescript/src/components/Tabs/Tabs.d.ts +44 -0
  32. package/lib/typescript/src/components/index.d.ts +6 -0
  33. package/lib/typescript/src/icons/registry.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Button/Button.tsx +14 -1
  36. package/src/components/Checkbox/Checkbox.tsx +238 -0
  37. package/src/components/MoneyValue/MoneyValue.tsx +134 -79
  38. package/src/components/NoteInput/NoteInput.tsx +146 -0
  39. package/src/components/NoteInput/index.ts +2 -0
  40. package/src/components/Numpad/Numpad.tsx +162 -0
  41. package/src/components/StatusHero/StatusHero.tsx +156 -0
  42. package/src/components/Tabs/TabItem.tsx +96 -0
  43. package/src/components/Tabs/Tabs.tsx +105 -0
  44. package/src/components/index.ts +6 -0
  45. package/src/icons/registry.ts +1 -1
@@ -1,5 +1,13 @@
1
- import React from 'react'
2
- import { View, Text, type StyleProp, type ViewStyle, type TextStyle } from 'react-native'
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?: string | number;
42
- currency?: string;
43
- negative?: boolean;
44
- modes?: Record<string, any>;
45
- style?: StyleProp<ViewStyle>;
46
- valueStyle?: StyleProp<TextStyle>;
47
- currencyStyle?: StyleProp<TextStyle>;
48
- negativeSignStyle?: StyleProp<TextStyle>;
49
- accessibilityLabel?: string;
50
- accessibilityHint?: string;
51
- } & React.ComponentProps<typeof View>;
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
- * and an optional `modes` configuration, consistent with other components in this library.
72
+ * The styling is fully resolved from Figma design tokens using `getVariableByName`.
73
+ * Supports separate typography scaling for currency and value.
58
74
  *
59
- * Currency can be provided either as a symbol (e.g. "₹") or as an ISO code
60
- * (e.g. "INR", "USD"). When an ISO code is provided, it will be mapped to the
61
- * appropriate symbol, with special care to support INR and other major currencies.
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 } = React.useMemo(() => {
97
+ const { isNegative, displayValue } = useMemo(() => {
95
98
  const stringValue = String(value)
96
99
  const trimmed = stringValue.trim()
97
- const valueIsNegative = trimmed.startsWith('-') || (typeof value === 'number' && value < 0)
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
- getVariableByName('moneyValue/text/color', modes) || '#0f0d0a'
114
- const fontSize =
115
- getVariableByName('moneyValue/supportText/fontSize', modes) || 14
116
- const lineHeight =
117
- getVariableByName('moneyValue/supportText/lineHeight', modes) || 18
118
- const fontWeightValue =
119
- getVariableByName('moneyValue/supportText/fontWeight', modes) || 500
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 = React.useMemo(() => {
133
+ const resolvedCurrency = useMemo(() => {
131
134
  if (!currency) return ''
132
135
  const upper = currency.toUpperCase ? currency.toUpperCase() : currency
133
- return (upper in CURRENCY_SYMBOLS ? CURRENCY_SYMBOLS[upper as keyof typeof CURRENCY_SYMBOLS] : currency)
136
+ return upper in CURRENCY_SYMBOLS
137
+ ? CURRENCY_SYMBOLS[upper as keyof typeof CURRENCY_SYMBOLS]
138
+ : currency
134
139
  }, [currency])
135
140
 
136
- const baseTextStyle = {
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
- // Generate default accessibility label from value and currency
151
- const defaultAccessibilityLabel = accessibilityLabel ||
152
- `${isNegative ? 'negative ' : ''}${resolvedCurrency} ${displayValue}`
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
- <View
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={[baseTextStyle, negativeSignStyle]}
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={[baseTextStyle, currencyStyle]}
211
+ <Text
212
+ style={[currencyTextStyle, currencyStyle]}
173
213
  accessibilityElementsHidden={true}
174
214
  importantForAccessibility="no"
175
215
  >
176
216
  {resolvedCurrency}
177
217
  </Text>
178
- <Text
179
- style={[baseTextStyle, valueStyle]}
180
- accessibilityElementsHidden={true}
181
- importantForAccessibility="no"
182
- >
183
- {displayValue}
184
- </Text>
185
- </View>
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,2 @@
1
+ export { default } from './NoteInput'
2
+ export type { NoteInputProps } from './NoteInput'
@@ -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