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.
Files changed (106) hide show
  1. package/lib/commonjs/components/AmountInput/AmountInput.js +82 -0
  2. package/lib/commonjs/components/AmountInput/index.js +13 -0
  3. package/lib/commonjs/components/Button/Button.js +45 -28
  4. package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +76 -0
  5. package/lib/commonjs/components/Checkbox/Checkbox.js +208 -0
  6. package/lib/commonjs/components/EmptyState/EmptyState.js +2 -1
  7. package/lib/commonjs/components/MoneyValue/MoneyValue.js +81 -49
  8. package/lib/commonjs/components/NoteInput/NoteInput.js +120 -0
  9. package/lib/commonjs/components/NoteInput/index.js +13 -0
  10. package/lib/commonjs/components/Numpad/Numpad.js +108 -0
  11. package/lib/commonjs/components/OTP/OTP.js +242 -0
  12. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +78 -0
  13. package/lib/commonjs/components/ProductLabel/ProductLabel.js +50 -0
  14. package/lib/commonjs/components/ProgressBadge/ProgressBadge.js +130 -0
  15. package/lib/commonjs/components/ProgressBadge/index.js +25 -0
  16. package/lib/commonjs/components/StatItem/StatItem.js +61 -0
  17. package/lib/commonjs/components/StatusHero/StatusHero.js +148 -0
  18. package/lib/commonjs/components/SwappableAmount/SwappableAmount.js +71 -0
  19. package/lib/commonjs/components/Tabs/TabItem.js +79 -0
  20. package/lib/commonjs/components/Tabs/Tabs.js +88 -0
  21. package/lib/commonjs/components/Text/Text.js +38 -0
  22. package/lib/commonjs/components/Toggle/Toggle.js +102 -0
  23. package/lib/commonjs/components/index.js +105 -0
  24. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -0
  25. package/lib/commonjs/design-tokens/figma-variables-resolver.js +1 -1
  26. package/lib/commonjs/icons/registry.js +1 -1
  27. package/lib/module/components/AmountInput/AmountInput.js +77 -0
  28. package/lib/module/components/AmountInput/index.js +3 -0
  29. package/lib/module/components/Button/Button.js +44 -28
  30. package/lib/module/components/CardProviderInfo/CardProviderInfo.js +71 -0
  31. package/lib/module/components/Checkbox/Checkbox.js +205 -0
  32. package/lib/module/components/EmptyState/EmptyState.js +2 -1
  33. package/lib/module/components/MoneyValue/MoneyValue.js +81 -49
  34. package/lib/module/components/NoteInput/NoteInput.js +115 -0
  35. package/lib/module/components/NoteInput/index.js +3 -0
  36. package/lib/module/components/Numpad/Numpad.js +103 -0
  37. package/lib/module/components/OTP/OTP.js +236 -0
  38. package/lib/module/components/PortfolioHero/PortfolioHero.js +73 -0
  39. package/lib/module/components/ProductLabel/ProductLabel.js +45 -0
  40. package/lib/module/components/ProgressBadge/ProgressBadge.js +125 -0
  41. package/lib/module/components/ProgressBadge/index.js +4 -0
  42. package/lib/module/components/StatItem/StatItem.js +56 -0
  43. package/lib/module/components/StatusHero/StatusHero.js +142 -0
  44. package/lib/module/components/SwappableAmount/SwappableAmount.js +66 -0
  45. package/lib/module/components/Tabs/TabItem.js +74 -0
  46. package/lib/module/components/Tabs/Tabs.js +78 -0
  47. package/lib/module/components/Text/Text.js +33 -0
  48. package/lib/module/components/Toggle/Toggle.js +97 -0
  49. package/lib/module/components/index.js +16 -1
  50. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -0
  51. package/lib/module/design-tokens/figma-variables-resolver.js +1 -1
  52. package/lib/module/icons/registry.js +1 -1
  53. package/lib/typescript/src/components/AmountInput/AmountInput.d.ts +23 -0
  54. package/lib/typescript/src/components/AmountInput/index.d.ts +3 -0
  55. package/lib/typescript/src/components/Button/Button.d.ts +6 -1
  56. package/lib/typescript/src/components/CardProviderInfo/CardProviderInfo.d.ts +24 -0
  57. package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +30 -0
  58. package/lib/typescript/src/components/EmptyState/EmptyState.d.ts +6 -1
  59. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +18 -26
  60. package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +23 -0
  61. package/lib/typescript/src/components/NoteInput/index.d.ts +3 -0
  62. package/lib/typescript/src/components/Numpad/Numpad.d.ts +35 -0
  63. package/lib/typescript/src/components/OTP/OTP.d.ts +36 -0
  64. package/lib/typescript/src/components/PortfolioHero/PortfolioHero.d.ts +21 -0
  65. package/lib/typescript/src/components/ProductLabel/ProductLabel.d.ts +14 -0
  66. package/lib/typescript/src/components/ProgressBadge/ProgressBadge.d.ts +36 -0
  67. package/lib/typescript/src/components/ProgressBadge/index.d.ts +3 -0
  68. package/lib/typescript/src/components/StatItem/StatItem.d.ts +21 -0
  69. package/lib/typescript/src/components/StatusHero/StatusHero.d.ts +47 -0
  70. package/lib/typescript/src/components/SwappableAmount/SwappableAmount.d.ts +22 -0
  71. package/lib/typescript/src/components/Tabs/TabItem.d.ts +29 -0
  72. package/lib/typescript/src/components/Tabs/Tabs.d.ts +44 -0
  73. package/lib/typescript/src/components/Text/Text.d.ts +14 -0
  74. package/lib/typescript/src/components/Toggle/Toggle.d.ts +29 -0
  75. package/lib/typescript/src/components/index.d.ts +15 -0
  76. package/lib/typescript/src/icons/registry.d.ts +1 -1
  77. package/package.json +1 -1
  78. package/src/components/AmountInput/AmountInput.tsx +81 -0
  79. package/src/components/AmountInput/index.ts +2 -0
  80. package/src/components/Button/Button.tsx +40 -20
  81. package/src/components/CardProviderInfo/CardProviderInfo.tsx +81 -0
  82. package/src/components/Checkbox/Checkbox.tsx +238 -0
  83. package/src/components/EmptyState/EmptyState.tsx +7 -1
  84. package/src/components/MoneyValue/MoneyValue.tsx +134 -79
  85. package/src/components/NoteInput/NoteInput.tsx +146 -0
  86. package/src/components/NoteInput/index.ts +2 -0
  87. package/src/components/Numpad/Numpad.tsx +162 -0
  88. package/src/components/OTP/OTP.tsx +275 -0
  89. package/src/components/PortfolioHero/PortfolioHero.tsx +91 -0
  90. package/src/components/ProductLabel/ProductLabel.tsx +58 -0
  91. package/src/components/ProgressBadge/ProgressBadge.tsx +172 -0
  92. package/src/components/ProgressBadge/index.ts +2 -0
  93. package/src/components/StatItem/StatItem.tsx +71 -0
  94. package/src/components/StatusHero/StatusHero.tsx +156 -0
  95. package/src/components/SwappableAmount/SwappableAmount.tsx +92 -0
  96. package/src/components/Tabs/TabItem.tsx +96 -0
  97. package/src/components/Tabs/Tabs.tsx +105 -0
  98. package/src/components/Text/Text.tsx +48 -0
  99. package/src/components/Toggle/Toggle.tsx +122 -0
  100. package/src/components/index.ts +15 -0
  101. package/src/design-tokens/Coin Variables-variables-full.json +1 -0
  102. package/src/design-tokens/figma-variables-resolver.ts +1 -1
  103. package/src/icons/registry.ts +1 -1
  104. package/lib/commonjs/design-tokens/JFS Variables-variables-full.json +0 -1
  105. package/lib/module/design-tokens/JFS Variables-variables-full.json +0 -1
  106. package/src/design-tokens/JFS Variables-variables-full.json +0 -1
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import React from 'react';
4
- import { View, Text } from 'react-native';
3
+ import React, { useMemo, useEffect, useRef } from 'react';
4
+ import { View, Text, Pressable, Animated } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
 
7
7
  // Map of common ISO 4217 currency codes to display symbols
@@ -42,34 +42,20 @@ const CURRENCY_SYMBOLS = {
42
42
  /**
43
43
  * MoneyValue component that mirrors the Figma MoneyValue design.
44
44
  *
45
- * The styling is fully resolved from Figma design tokens using `getVariableByName`
46
- * and an optional `modes` configuration, consistent with other components in this library.
45
+ * The styling is fully resolved from Figma design tokens using `getVariableByName`.
46
+ * Supports separate typography scaling for currency and value.
47
47
  *
48
- * Currency can be provided either as a symbol (e.g. "₹") or as an ISO code
49
- * (e.g. "INR", "USD"). When an ISO code is provided, it will be mapped to the
50
- * appropriate symbol, with special care to support INR and other major currencies.
51
- *
52
- * Negative values are auto-detected from the value prop (e.g., -500 or "-500").
53
- * The `negative` prop can be used to explicitly override this behavior.
54
- *
55
- * @component
56
- * @param {Object} props
57
- * @param {string|number} [props.value="500"] - Monetary value to display. Negative values are auto-detected.
58
- * @param {string} [props.currency="₹"] - Currency symbol or ISO code (e.g. "INR").
59
- * @param {boolean} [props.negative] - Explicitly override negative display. If undefined, auto-detects from value.
60
- * @param {Object} [props.modes={}] - Modes object passed directly to `getVariableByName`.
61
- * Example: {"MoneyValue / Theme": "Default"}
62
- * @param {Object} [props.style] - Optional container style overrides.
63
- * @param {Object} [props.valueStyle] - Optional value text style overrides.
64
- * @param {Object} [props.currencyStyle] - Optional currency text style overrides.
65
- * @param {Object} [props.negativeSignStyle] - Optional negative sign text style overrides.
66
- * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers. If not provided, generates from value and currency
67
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
48
+ * To make this component editable without coupling it to a Numpad component,
49
+ * use the `onPress` prop and manage the data state in the parent layer. When
50
+ * the `focused` prop is provided to this component, it will display a natural
51
+ * blinking text cursor.
68
52
  */
69
53
  function MoneyValue({
70
54
  value = '500',
71
55
  currency = '₹',
72
56
  negative,
57
+ focused = false,
58
+ hidden = false,
73
59
  modes = {},
74
60
  style,
75
61
  valueStyle,
@@ -77,13 +63,14 @@ function MoneyValue({
77
63
  negativeSignStyle,
78
64
  accessibilityLabel,
79
65
  accessibilityHint,
66
+ onPress,
80
67
  ...rest
81
68
  }) {
82
69
  // Auto-detect negative from value and compute display value
83
70
  const {
84
71
  isNegative,
85
72
  displayValue
86
- } = React.useMemo(() => {
73
+ } = useMemo(() => {
87
74
  const stringValue = String(value);
88
75
  const trimmed = stringValue.trim();
89
76
  const valueIsNegative = trimmed.startsWith('-') || typeof value === 'number' && value < 0;
@@ -94,62 +81,107 @@ function MoneyValue({
94
81
  // Use explicit negative prop if provided, otherwise use auto-detected
95
82
  const showNegative = negative !== undefined ? negative : valueIsNegative;
96
83
  return {
97
- isNegative: showNegative,
98
- displayValue: absoluteValue
84
+ isNegative: hidden ? false : showNegative,
85
+ displayValue: hidden ? '•••' : absoluteValue
99
86
  };
100
- }, [value, negative]);
87
+ }, [value, negative, hidden]);
101
88
 
102
89
  // Resolve typography and layout tokens from Figma
103
90
  const textColor = getVariableByName('moneyValue/text/color', modes) || '#0f0d0a';
104
- const fontSize = getVariableByName('moneyValue/supportText/fontSize', modes) || 14;
105
- const lineHeight = getVariableByName('moneyValue/supportText/lineHeight', modes) || 18;
106
- const fontWeightValue = getVariableByName('moneyValue/supportText/fontWeight', modes) || 500;
91
+ const currencyFontSize = getVariableByName('moneyValue/currency/fontSize', modes) || 14;
92
+ const currencyLineHeight = getVariableByName('moneyValue/currency/lineHeight', modes) || 18;
93
+ const valueFontSize = getVariableByName('moneyValue/value/fontSize', modes) || 14;
94
+ const valueLineHeight = getVariableByName('moneyValue/value/lineHeight', modes) || 18;
95
+ const fontWeightValue = getVariableByName('moneyValue/fontWeight', modes) || 500;
107
96
  const fontWeight = typeof fontWeightValue === 'number' ? fontWeightValue.toString() : fontWeightValue;
108
- const fontFamily = getVariableByName('moneyValue/supportText/fontFamily', modes) || 'System';
97
+ const fontFamily = getVariableByName('moneyValue/fontFamily', modes) || 'System';
109
98
  const gap = getVariableByName('moneyValue/gap', modes) || 4;
110
99
 
111
100
  // Resolve currency to a symbol, supporting both symbols and ISO codes
112
- const resolvedCurrency = React.useMemo(() => {
101
+ const resolvedCurrency = useMemo(() => {
113
102
  if (!currency) return '';
114
103
  const upper = currency.toUpperCase ? currency.toUpperCase() : currency;
115
104
  return upper in CURRENCY_SYMBOLS ? CURRENCY_SYMBOLS[upper] : currency;
116
105
  }, [currency]);
117
- const baseTextStyle = {
106
+ const currencyTextStyle = {
107
+ color: textColor,
108
+ fontSize: currencyFontSize,
109
+ lineHeight: currencyLineHeight,
110
+ fontWeight: fontWeight,
111
+ fontFamily: fontFamily
112
+ };
113
+ const valueTextStyle = {
118
114
  color: textColor,
119
- fontSize,
120
- lineHeight,
121
- fontWeight,
122
- fontFamily
115
+ fontSize: valueFontSize,
116
+ lineHeight: valueLineHeight,
117
+ fontWeight: fontWeight,
118
+ fontFamily: fontFamily
123
119
  };
124
120
  const containerStyle = {
125
121
  flexDirection: 'row',
126
122
  alignItems: 'center',
127
- gap
123
+ gap: gap
128
124
  };
129
125
 
130
- // Generate default accessibility label from value and currency
131
- const defaultAccessibilityLabel = accessibilityLabel || `${isNegative ? 'negative ' : ''}${resolvedCurrency} ${displayValue}`;
132
- return /*#__PURE__*/_jsxs(View, {
126
+ // Blinking cursor animation
127
+ const cursorOpacity = useRef(new Animated.Value(0)).current;
128
+ useEffect(() => {
129
+ if (focused) {
130
+ const animation = Animated.loop(Animated.sequence([Animated.timing(cursorOpacity, {
131
+ toValue: 1,
132
+ duration: 400,
133
+ useNativeDriver: true
134
+ }), Animated.timing(cursorOpacity, {
135
+ toValue: 0,
136
+ duration: 400,
137
+ useNativeDriver: true
138
+ })]));
139
+ animation.start();
140
+ return () => {
141
+ animation.stop();
142
+ cursorOpacity.setValue(0);
143
+ };
144
+ } else {
145
+ cursorOpacity.setValue(0);
146
+ }
147
+ }, [focused, cursorOpacity]);
148
+ return /*#__PURE__*/_jsxs(Pressable, {
133
149
  style: [containerStyle, style],
134
150
  accessibilityRole: "text",
135
151
  accessibilityLabel: undefined,
136
152
  accessibilityHint: accessibilityHint,
153
+ onPress: onPress,
154
+ disabled: !onPress,
137
155
  ...rest,
138
156
  children: [isNegative && /*#__PURE__*/_jsx(Text, {
139
- style: [baseTextStyle, negativeSignStyle],
157
+ style: [currencyTextStyle, negativeSignStyle],
140
158
  accessibilityElementsHidden: true,
141
159
  importantForAccessibility: "no",
142
160
  children: "-"
143
161
  }), /*#__PURE__*/_jsx(Text, {
144
- style: [baseTextStyle, currencyStyle],
162
+ style: [currencyTextStyle, currencyStyle],
145
163
  accessibilityElementsHidden: true,
146
164
  importantForAccessibility: "no",
147
165
  children: resolvedCurrency
148
- }), /*#__PURE__*/_jsx(Text, {
149
- style: [baseTextStyle, valueStyle],
150
- accessibilityElementsHidden: true,
151
- importantForAccessibility: "no",
152
- children: displayValue
166
+ }), /*#__PURE__*/_jsxs(View, {
167
+ style: {
168
+ flexDirection: 'row',
169
+ alignItems: 'center'
170
+ },
171
+ children: [/*#__PURE__*/_jsx(Text, {
172
+ style: [valueTextStyle, valueStyle],
173
+ accessibilityElementsHidden: true,
174
+ importantForAccessibility: "no",
175
+ children: displayValue
176
+ }), focused && /*#__PURE__*/_jsx(Animated.View, {
177
+ style: {
178
+ opacity: cursorOpacity,
179
+ width: 2,
180
+ height: valueFontSize * 1.1,
181
+ backgroundColor: textColor,
182
+ marginLeft: 2
183
+ }
184
+ })]
153
185
  })]
154
186
  });
155
187
  }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+
3
+ import React, { useState, useRef } from 'react';
4
+ import { View, TextInput as RNTextInput, Text, Pressable } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ /**
8
+ * NoteInput component representing an interactive "Add note" badge style field.
9
+ * Allows the user to click, clears the placeholder text, and shows a blinking cursor when focused.
10
+ */
11
+ export default function NoteInput({
12
+ value = '',
13
+ placeholder = 'Add note',
14
+ onChangeText,
15
+ modes = {},
16
+ style,
17
+ textStyle,
18
+ state: explicitState,
19
+ onFocus,
20
+ onBlur,
21
+ ...rest
22
+ }) {
23
+ const [internalFocused, setInternalFocused] = useState(false);
24
+ const inputRef = useRef(null);
25
+
26
+ // Resolve tokens from Figma Design
27
+ const foreground = getVariableByName('noteInput/foreground', modes) || '#0d0d0f';
28
+ const fontSize = getVariableByName('noteInput/fontSize', modes) || 14;
29
+ const fontFamily = getVariableByName('noteInput/fontFamily', modes) || 'JioType Var';
30
+ const lineHeight = getVariableByName('noteInput/lineHeight', modes) || 16;
31
+ const fontWeightRaw = getVariableByName('noteInput/fontWeight', modes) || 700;
32
+ const fontWeight = typeof fontWeightRaw === 'number' ? fontWeightRaw.toString() : fontWeightRaw;
33
+ const gap = getVariableByName('noteInput/gap', modes) || 0; // 4 in some files, 0 in context
34
+ const paddingHorizontal = getVariableByName('noteInput/padding/horizontal', modes) || 17;
35
+ const paddingVertical = getVariableByName('noteInput/padding/vertical', modes) || 9;
36
+ const radius = getVariableByName('noteInput/radius', modes) || 999;
37
+ const borderSize = getVariableByName('noteInput/border/size', modes) || 1;
38
+ const background = getVariableByName('noteInput/background', modes) || '#ebebed';
39
+ const borderColor = getVariableByName('noteInput/border/color', modes) || 'rgba(255,255,255,0)';
40
+ const containerStyle = {
41
+ flexDirection: 'row',
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ paddingHorizontal: paddingHorizontal,
45
+ paddingVertical: paddingVertical,
46
+ borderRadius: radius,
47
+ backgroundColor: background,
48
+ borderWidth: borderSize,
49
+ borderColor: borderColor,
50
+ gap: gap,
51
+ // Add specific width when editing if requested by Figma design logic, though flex fits content generically
52
+ alignSelf: 'flex-start'
53
+ };
54
+ const baseTextStyle = {
55
+ color: foreground,
56
+ fontSize: fontSize,
57
+ fontFamily: fontFamily,
58
+ lineHeight: lineHeight,
59
+ fontWeight: fontWeight,
60
+ padding: 0,
61
+ margin: 0,
62
+ minHeight: lineHeight
63
+ };
64
+ const handleFocus = e => {
65
+ setInternalFocused(true);
66
+ if (onFocus) onFocus(e);
67
+ };
68
+ const handleBlur = e => {
69
+ setInternalFocused(false);
70
+ if (onBlur) onBlur(e);
71
+ };
72
+ const handlePress = () => {
73
+ inputRef.current?.focus();
74
+ };
75
+
76
+ // Blinking cursor setup for custom UI representation if we were drawing it natively.
77
+ // We use RNTextInput's native cursor, but we can style it or ensure it's visible.
78
+
79
+ return /*#__PURE__*/_jsx(Pressable, {
80
+ style: [containerStyle, style],
81
+ onPress: handlePress,
82
+ children: /*#__PURE__*/_jsxs(View, {
83
+ style: {
84
+ position: 'relative',
85
+ justifyContent: 'center'
86
+ },
87
+ children: [/*#__PURE__*/_jsx(Text, {
88
+ style: [baseTextStyle, textStyle, {
89
+ opacity: 0
90
+ }],
91
+ accessibilityElementsHidden: true,
92
+ importantForAccessibility: "no",
93
+ children: internalFocused ? value || ' ' : value || placeholder
94
+ }), /*#__PURE__*/_jsx(RNTextInput, {
95
+ ref: inputRef,
96
+ value: value,
97
+ onChangeText: onChangeText,
98
+ placeholder: internalFocused ? '' : placeholder,
99
+ placeholderTextColor: foreground,
100
+ onFocus: handleFocus,
101
+ onBlur: handleBlur,
102
+ selectionColor: foreground,
103
+ style: [baseTextStyle, {
104
+ position: 'absolute',
105
+ left: 0,
106
+ right: 0,
107
+ top: 0,
108
+ bottom: 0,
109
+ outlineStyle: 'none'
110
+ }, textStyle],
111
+ ...rest
112
+ })]
113
+ })
114
+ });
115
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ export { default } from './NoteInput';
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo, useCallback } from 'react';
4
+ import { View, Text, Pressable } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { IconDeletebackspace } from '../../icons/components/IconDeletebackspace';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ function shuffleArray(arr) {
9
+ const shuffled = [...arr];
10
+ for (let i = shuffled.length - 1; i > 0; i--) {
11
+ const j = Math.floor(Math.random() * (i + 1));
12
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
13
+ }
14
+ return shuffled;
15
+ }
16
+
17
+ /**
18
+ * Secure numpad component for the JFS finance system.
19
+ *
20
+ * Renders a 3×4 grid of digit keys (0-9), an optional decimal key, and a
21
+ * backspace key. Digit positions are shuffled by default to guard against
22
+ * keylogging and shoulder-surfing attacks on mobile devices.
23
+ *
24
+ * @component
25
+ * @param {NumpadProps} props
26
+ */
27
+ function Numpad({
28
+ modes = {},
29
+ onKeyPress,
30
+ showDecimal = true,
31
+ shuffle = true,
32
+ style,
33
+ keyStyle,
34
+ keyTextStyle
35
+ }) {
36
+ const foreground = getVariableByName('numpad/foreground', modes) ?? '#141414';
37
+ const lineHeight = getVariableByName('numpad/lineHeight', modes) ?? 32;
38
+ const fontFamily = getVariableByName('numpad/fontFamily', modes) ?? 'JioType Var';
39
+ const fontSize = getVariableByName('numpad/fontSize', modes) ?? 32;
40
+ const rowGap = getVariableByName('numpad/gridRowGap/vertical', modes) ?? 12;
41
+ const columnGap = getVariableByName('numpad/gridColumnGap/horizontal', modes) ?? 12;
42
+ const digits = useMemo(() => {
43
+ const base = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
44
+ return shuffle ? shuffleArray(base) : base;
45
+ }, [shuffle]);
46
+ const rows = useMemo(() => [digits.slice(0, 3), digits.slice(3, 6), digits.slice(6, 9), [showDecimal ? '.' : null, digits[9], 'backspace']], [digits, showDecimal]);
47
+ const handlePress = useCallback(key => {
48
+ onKeyPress?.(key);
49
+ }, [onKeyPress]);
50
+ const textStyle = {
51
+ color: foreground,
52
+ fontFamily: fontFamily,
53
+ fontSize: fontSize,
54
+ lineHeight: lineHeight,
55
+ textAlign: 'center'
56
+ };
57
+ return /*#__PURE__*/_jsx(View, {
58
+ style: [{
59
+ gap: rowGap
60
+ }, style],
61
+ accessibilityRole: "none",
62
+ children: rows.map((row, rowIndex) => /*#__PURE__*/_jsx(View, {
63
+ style: {
64
+ flexDirection: 'row',
65
+ gap: columnGap
66
+ },
67
+ children: row.map((key, colIndex) => {
68
+ if (key === null) {
69
+ return /*#__PURE__*/_jsx(View, {
70
+ style: {
71
+ flex: 1
72
+ }
73
+ }, `empty-${colIndex}`);
74
+ }
75
+ const isBackspace = key === 'backspace';
76
+ return /*#__PURE__*/_jsx(Pressable, {
77
+ style: ({
78
+ pressed
79
+ }) => [{
80
+ flex: 1,
81
+ justifyContent: 'center',
82
+ alignItems: 'center',
83
+ minHeight: 46
84
+ }, pressed && {
85
+ opacity: 0.4
86
+ }, keyStyle],
87
+ onPress: () => handlePress(key),
88
+ accessibilityRole: "button",
89
+ accessibilityLabel: isBackspace ? 'Backspace' : key,
90
+ children: isBackspace ? /*#__PURE__*/_jsx(IconDeletebackspace, {
91
+ width: fontSize,
92
+ height: fontSize,
93
+ fill: foreground
94
+ }) : /*#__PURE__*/_jsx(Text, {
95
+ style: [textStyle, keyTextStyle],
96
+ children: key
97
+ })
98
+ }, `${key}-${colIndex}`);
99
+ })
100
+ }, rowIndex))
101
+ });
102
+ }
103
+ export default Numpad;
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+
3
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { View, Text, TextInput as RNTextInput, Pressable, Animated } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import SupportText from '../SupportText/SupportText';
8
+ import { cloneChildrenWithModes } from '../../utils/react-utils';
9
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
10
+ const DIGITS_ONLY = /^\d*$/;
11
+ function OTP({
12
+ length = 6,
13
+ value: controlledValue,
14
+ defaultValue = '',
15
+ onChange,
16
+ onValueChange,
17
+ onComplete,
18
+ isDisabled = false,
19
+ isInvalid = false,
20
+ allowedPattern = DIGITS_ONLY,
21
+ autoFocus = false,
22
+ modes: propModes = {},
23
+ style,
24
+ supportText,
25
+ supportTextStatus
26
+ }) {
27
+ const {
28
+ modes: globalModes
29
+ } = useTokens();
30
+ const modes = {
31
+ ...globalModes,
32
+ ...propModes
33
+ };
34
+ const isControlled = controlledValue !== undefined;
35
+ const [internalValue, setInternalValue] = useState(defaultValue);
36
+ const currentValue = isControlled ? controlledValue : internalValue;
37
+ const inputRef = useRef(null);
38
+ const [isFocused, setIsFocused] = useState(false);
39
+ const caretAnim = useRef(new Animated.Value(1)).current;
40
+ useEffect(() => {
41
+ if (!isFocused) return;
42
+ const blink = Animated.loop(Animated.sequence([Animated.timing(caretAnim, {
43
+ toValue: 0,
44
+ duration: 400,
45
+ useNativeDriver: true
46
+ }), Animated.timing(caretAnim, {
47
+ toValue: 1,
48
+ duration: 400,
49
+ useNativeDriver: true
50
+ })]));
51
+ blink.start();
52
+ return () => blink.stop();
53
+ }, [isFocused, caretAnim]);
54
+ const prevCompleteRef = useRef(false);
55
+ const setValue = useCallback(next => {
56
+ const clamped = next.slice(0, length);
57
+ if (!isControlled) setInternalValue(clamped);
58
+ onChange?.(clamped);
59
+ onValueChange?.(clamped);
60
+ if (clamped.length === length && !prevCompleteRef.current) {
61
+ prevCompleteRef.current = true;
62
+ onComplete?.(clamped);
63
+ }
64
+ if (clamped.length < length) {
65
+ prevCompleteRef.current = false;
66
+ }
67
+ }, [length, isControlled, onChange, onValueChange, onComplete]);
68
+ const handleChangeText = useCallback(text => {
69
+ if (isDisabled) return;
70
+ if (!allowedPattern.test(text)) return;
71
+ setValue(text);
72
+ }, [isDisabled, allowedPattern, setValue]);
73
+ const handlePress = useCallback(() => {
74
+ if (isDisabled) return;
75
+ inputRef.current?.focus();
76
+ }, [isDisabled]);
77
+
78
+ // --- Token resolution (state-independent tokens resolved once) ---
79
+ const otpGap = Number(getVariableByName('otp/gap', modes)) || 36;
80
+ const otpPaddingH = Number(getVariableByName('otp/padding/horizontal', modes)) || 8;
81
+ const otpPaddingV = Number(getVariableByName('otp/padding/vertical', modes)) || 8;
82
+ const slotWidth = Number(getVariableByName('pinSlot/width', modes)) || 48;
83
+ const slotGap = Number(getVariableByName('pinSlot/gap', modes)) || 8;
84
+ // digit/color has no state variants in Figma — resolved once from the Output collection
85
+ const digitColor = getVariableByName('pinSlot/digit/color', modes) || '#000000';
86
+ const digitFontSize = Number(getVariableByName('pinSlot/digit/fontSize', modes)) || 24;
87
+ const digitFontFamily = getVariableByName('pinSlot/digit/fontFamily', modes) || 'JioType Var';
88
+ const digitLineHeight = Number(getVariableByName('pinSlot/digit/lineHeight', modes)) || 29;
89
+ const digitFontWeight = getVariableByName('pinSlot/digit/fontWeight', modes) || '500';
90
+ const underlineHeight = Number(getVariableByName('pinSlot/underline/height', modes)) || 2;
91
+ const underlineRadius = Number(getVariableByName('pinSlot/underline/radius', modes)) || 1;
92
+
93
+ // --- State-driven slot modes ---
94
+ // Collection name in Figma is "Input/PINSlot States" (double space before States).
95
+ // Only PinSlot/underline/color (capital P/S) lives in this collection with Idle/Active/Error modes.
96
+ // isInvalid takes priority over active focus; the component maps semantic state → token mode
97
+ // internally so consumers never need to know the collection key name.
98
+ const getSlotModes = isActiveSlot => {
99
+ if (isInvalid) return {
100
+ ...modes,
101
+ 'Input/PINSlot States': 'Error'
102
+ };
103
+ if (isActiveSlot && isFocused) return {
104
+ ...modes,
105
+ 'Input/PINSlot States': 'Active'
106
+ };
107
+ return {
108
+ ...modes,
109
+ 'Input/PINSlot States': 'Idle'
110
+ };
111
+ };
112
+
113
+ // --- Styles ---
114
+ const containerStyle = {
115
+ flexDirection: 'column',
116
+ alignItems: 'flex-end',
117
+ gap: otpGap,
118
+ paddingHorizontal: otpPaddingH,
119
+ paddingVertical: otpPaddingV
120
+ };
121
+ const slotWrapStyle = {
122
+ flexDirection: 'row',
123
+ gap: 8,
124
+ alignItems: 'flex-start',
125
+ alignSelf: 'stretch'
126
+ };
127
+ const renderSlot = index => {
128
+ const char = currentValue[index];
129
+ const isActiveSlot = index === currentValue.length && currentValue.length < length;
130
+ const isFilled = char !== undefined;
131
+
132
+ // Underline color is the only state-sensitive token (lives in "Input/PINSlot States" collection).
133
+ // Note: token name is "PinSlot/underline/color" (capital P/S) — different from the static
134
+ // "pinSlot/underline/color" in the Output collection.
135
+ const slotModes = getSlotModes(isActiveSlot);
136
+ const underlineColor = getVariableByName('PinSlot/underline/color', slotModes) || '#303338';
137
+ const slotStyle = {
138
+ width: slotWidth,
139
+ flexDirection: 'column',
140
+ alignItems: 'center',
141
+ justifyContent: 'flex-end',
142
+ gap: slotGap
143
+ };
144
+ const digitStyle = {
145
+ fontFamily: digitFontFamily,
146
+ fontWeight: digitFontWeight,
147
+ fontSize: digitFontSize,
148
+ lineHeight: digitLineHeight,
149
+ color: digitColor,
150
+ textAlign: 'center',
151
+ minWidth: '100%'
152
+ };
153
+ const underlineStyle = {
154
+ width: slotWidth,
155
+ height: underlineHeight,
156
+ borderRadius: underlineRadius,
157
+ backgroundColor: underlineColor
158
+ };
159
+ return /*#__PURE__*/_jsxs(View, {
160
+ style: slotStyle,
161
+ children: [/*#__PURE__*/_jsx(View, {
162
+ style: {
163
+ minHeight: digitLineHeight,
164
+ justifyContent: 'center',
165
+ alignItems: 'center',
166
+ width: '100%'
167
+ },
168
+ children: isFilled ? /*#__PURE__*/_jsx(Text, {
169
+ style: digitStyle,
170
+ children: char
171
+ }) : isActiveSlot && isFocused ? /*#__PURE__*/_jsx(Animated.View, {
172
+ style: {
173
+ width: 2,
174
+ height: digitFontSize,
175
+ backgroundColor: digitColor,
176
+ opacity: caretAnim
177
+ }
178
+ }) : /*#__PURE__*/_jsx(Text, {
179
+ style: [digitStyle, {
180
+ color: 'transparent'
181
+ }],
182
+ children: '\u00A0'
183
+ })
184
+ }), /*#__PURE__*/_jsx(View, {
185
+ style: underlineStyle
186
+ })]
187
+ }, index);
188
+ };
189
+ const renderSupportText = () => {
190
+ if (!supportText) return null;
191
+ if (typeof supportText === 'string') {
192
+ return /*#__PURE__*/_jsx(SupportText, {
193
+ label: supportText,
194
+ status: supportTextStatus ?? (isInvalid ? 'Error' : 'Neutral'),
195
+ modes: modes
196
+ });
197
+ }
198
+ return /*#__PURE__*/_jsx(_Fragment, {
199
+ children: cloneChildrenWithModes(React.Children.toArray(supportText), modes)
200
+ });
201
+ };
202
+ return /*#__PURE__*/_jsxs(Pressable, {
203
+ style: [containerStyle, isDisabled && {
204
+ opacity: 0.4
205
+ }, style],
206
+ onPress: handlePress,
207
+ disabled: isDisabled,
208
+ accessibilityRole: "none",
209
+ children: [/*#__PURE__*/_jsx(RNTextInput, {
210
+ ref: inputRef,
211
+ value: currentValue,
212
+ onChangeText: handleChangeText,
213
+ maxLength: length,
214
+ keyboardType: "number-pad",
215
+ autoFocus: autoFocus,
216
+ editable: !isDisabled,
217
+ onFocus: () => setIsFocused(true),
218
+ onBlur: () => setIsFocused(false),
219
+ caretHidden: true,
220
+ style: {
221
+ position: 'absolute',
222
+ width: 1,
223
+ height: 1,
224
+ opacity: 0
225
+ },
226
+ accessibilityLabel: `OTP input, ${length} digits`,
227
+ accessibilityHint: "Enter your verification code"
228
+ }), /*#__PURE__*/_jsx(View, {
229
+ style: slotWrapStyle,
230
+ children: Array.from({
231
+ length
232
+ }, (_, i) => renderSlot(i))
233
+ }), renderSupportText()]
234
+ });
235
+ }
236
+ export default OTP;