jfs-components 0.0.74 → 0.0.78

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 (146) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
  4. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  5. package/lib/commonjs/components/Badge/Badge.js +23 -0
  6. package/lib/commonjs/components/Button/Button.js +37 -0
  7. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  8. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  9. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  10. package/lib/commonjs/components/FormField/FormField.js +14 -1
  11. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  12. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  13. package/lib/commonjs/components/Image/Image.js +26 -1
  14. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  15. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  17. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  18. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  19. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  20. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  21. package/lib/commonjs/components/PageHero/PageHero.js +41 -5
  22. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  23. package/lib/commonjs/components/Stepper/Step.js +47 -60
  24. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  25. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  26. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  27. package/lib/commonjs/components/Text/Text.js +31 -1
  28. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  29. package/lib/commonjs/components/Title/Title.js +10 -2
  30. package/lib/commonjs/components/index.js +35 -0
  31. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  32. package/lib/commonjs/icons/Icon.js +16 -0
  33. package/lib/commonjs/icons/registry.js +1 -1
  34. package/lib/commonjs/index.js +12 -0
  35. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  36. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  37. package/lib/commonjs/skeleton/index.js +58 -0
  38. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  39. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  40. package/lib/module/components/Accordion/Accordion.js +56 -56
  41. package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
  42. package/lib/module/components/Avatar/Avatar.js +19 -0
  43. package/lib/module/components/Badge/Badge.js +23 -0
  44. package/lib/module/components/Button/Button.js +37 -0
  45. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  46. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  47. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  48. package/lib/module/components/FormField/FormField.js +16 -3
  49. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  50. package/lib/module/components/IconButton/IconButton.js +20 -0
  51. package/lib/module/components/Image/Image.js +25 -1
  52. package/lib/module/components/ListItem/ListItem.js +25 -10
  53. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  54. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  55. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  56. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  57. package/lib/module/components/MessageField/MessageField.js +313 -0
  58. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  59. package/lib/module/components/PageHero/PageHero.js +41 -5
  60. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  61. package/lib/module/components/Stepper/Step.js +48 -61
  62. package/lib/module/components/Stepper/StepLabel.js +40 -10
  63. package/lib/module/components/Stepper/Stepper.js +15 -17
  64. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  65. package/lib/module/components/Text/Text.js +31 -1
  66. package/lib/module/components/TextInput/TextInput.js +17 -2
  67. package/lib/module/components/Title/Title.js +10 -2
  68. package/lib/module/components/index.js +5 -0
  69. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/lib/module/icons/Icon.js +16 -0
  71. package/lib/module/icons/registry.js +1 -1
  72. package/lib/module/index.js +2 -1
  73. package/lib/module/skeleton/Skeleton.js +229 -0
  74. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  75. package/lib/module/skeleton/index.js +6 -0
  76. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  77. package/lib/module/skeleton/useReducedMotion.js +61 -0
  78. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  79. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  80. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  81. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  82. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  83. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  84. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  85. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  86. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  87. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  88. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  89. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  90. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  91. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  92. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  93. package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
  94. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  95. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  96. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  97. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  98. package/lib/typescript/src/components/Text/Text.d.ts +20 -1
  99. package/lib/typescript/src/components/index.d.ts +8 -3
  100. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  101. package/lib/typescript/src/icons/registry.d.ts +1 -1
  102. package/lib/typescript/src/index.d.ts +1 -0
  103. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  104. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  105. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  106. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  107. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  108. package/package.json +11 -1
  109. package/src/components/Accordion/Accordion.tsx +113 -73
  110. package/src/components/ActionFooter/ActionFooter.tsx +210 -92
  111. package/src/components/Avatar/Avatar.tsx +26 -0
  112. package/src/components/Badge/Badge.tsx +27 -0
  113. package/src/components/Button/Button.tsx +40 -0
  114. package/src/components/Checkbox/Checkbox.tsx +22 -9
  115. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  116. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  117. package/src/components/FormField/FormField.tsx +19 -3
  118. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  119. package/src/components/IconButton/IconButton.tsx +27 -0
  120. package/src/components/Image/Image.tsx +25 -0
  121. package/src/components/ListItem/ListItem.tsx +21 -10
  122. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  123. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  124. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  125. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  126. package/src/components/MessageField/MessageField.tsx +543 -0
  127. package/src/components/NavArrow/NavArrow.tsx +81 -17
  128. package/src/components/PageHero/PageHero.tsx +61 -4
  129. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  130. package/src/components/Stepper/Step.tsx +52 -51
  131. package/src/components/Stepper/StepLabel.tsx +46 -9
  132. package/src/components/Stepper/Stepper.tsx +20 -15
  133. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  134. package/src/components/Text/Text.tsx +54 -0
  135. package/src/components/TextInput/TextInput.tsx +14 -1
  136. package/src/components/Title/Title.tsx +13 -2
  137. package/src/components/index.ts +8 -3
  138. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  139. package/src/icons/Icon.tsx +17 -0
  140. package/src/icons/registry.ts +1 -1
  141. package/src/index.ts +1 -0
  142. package/src/skeleton/Skeleton.tsx +298 -0
  143. package/src/skeleton/SkeletonGroup.tsx +193 -0
  144. package/src/skeleton/index.ts +10 -0
  145. package/src/skeleton/shimmer-tokens.ts +221 -0
  146. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -0,0 +1,481 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Platform, Pressable, Text, TextInput as RNTextInput, View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import SupportText from '../SupportText/SupportText';
9
+ import Dropdown, { DropdownItem } from '../Dropdown/Dropdown';
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ const IS_WEB = Platform.OS === 'web';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Suggestions accept either a bare string (used as both value and label) or a
19
+ * full `{ value, label }` option object for richer data.
20
+ */
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function toNumber(value, fallback) {
27
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
28
+ if (typeof value === 'string') {
29
+ const parsed = parseFloat(value);
30
+ if (Number.isFinite(parsed)) return parsed;
31
+ }
32
+ return fallback;
33
+ }
34
+ function normalizeItem(item) {
35
+ if (typeof item === 'string') return {
36
+ value: item,
37
+ label: item
38
+ };
39
+ return item;
40
+ }
41
+ const defaultFilter = (query, option) => option.label.toLowerCase().includes(query.toLowerCase());
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Token resolution
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function useFormFieldTokens(modes) {
48
+ return useMemo(() => {
49
+ const labelColor = getVariableByName('formField/label/color', modes) || '#000000';
50
+ const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
51
+ const labelFontSize = toNumber(getVariableByName('formField/label/fontSize', modes), 14);
52
+ const labelLineHeight = toNumber(getVariableByName('formField/label/lineHeight', modes), 17);
53
+ const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
54
+ const gap = toNumber(getVariableByName('formField/gap', modes), 8);
55
+ const inputPaddingH = toNumber(getVariableByName('formField/input/padding/horizontal', modes), 12);
56
+ const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8);
57
+ const inputRadius = toNumber(getVariableByName('formField/input/radius', modes), 8);
58
+ const inputBorderSize = toNumber(getVariableByName('formField/input/border/size', modes), 1.5);
59
+ const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
60
+ const inputBorderColor = getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
61
+ const inputFontSize = toNumber(getVariableByName('formField/input/label/fontSize', modes), 16);
62
+ const inputLineHeight = toNumber(getVariableByName('formField/input/label/lineHeight', modes), 45);
63
+ const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
64
+ const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
65
+ const inputTextColor = getVariableByName('formField/input/label/color', modes) || '#24262b';
66
+ return {
67
+ labelColor,
68
+ labelFontFamily,
69
+ labelFontSize,
70
+ labelLineHeight,
71
+ labelFontWeight,
72
+ gap,
73
+ inputPaddingH,
74
+ inputGap,
75
+ inputRadius,
76
+ inputBorderSize,
77
+ inputBackground,
78
+ inputBorderColor,
79
+ inputFontSize,
80
+ inputLineHeight,
81
+ inputFontFamily,
82
+ inputFontWeight,
83
+ inputTextColor
84
+ };
85
+ }, [modes]);
86
+ }
87
+ function useDropdownItemTextTokens(modes) {
88
+ return useMemo(() => {
89
+ const foreground = getVariableByName('dropdownItem/foreground', modes) || '#000000';
90
+ const fontFamily = getVariableByName('dropdownItem/fontFamily', modes) || 'JioType Var';
91
+ const fontSize = toNumber(getVariableByName('dropdownItem/fontSize', modes), 16);
92
+ const lineHeight = toNumber(getVariableByName('dropdownItem/lineHeight', modes), 19);
93
+ return {
94
+ foreground,
95
+ fontFamily,
96
+ fontSize,
97
+ lineHeight
98
+ };
99
+ }, [modes]);
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Component
104
+ // ---------------------------------------------------------------------------
105
+
106
+ function SuggestiveSearch({
107
+ label,
108
+ placeholder = 'Search',
109
+ items,
110
+ inputValue,
111
+ defaultInputValue = '',
112
+ onInputChange,
113
+ value,
114
+ defaultValue = null,
115
+ onValueChange,
116
+ filter = defaultFilter,
117
+ minChars = 1,
118
+ maxResults,
119
+ highlightMatch = true,
120
+ emptyMessage,
121
+ renderItem,
122
+ open,
123
+ defaultOpen = false,
124
+ onOpenChange,
125
+ menuMaxHeight = 240,
126
+ menuOffset = 6,
127
+ isRequired = false,
128
+ isDisabled = false,
129
+ isInvalid = false,
130
+ isReadOnly = false,
131
+ supportText,
132
+ errorMessage,
133
+ modes: propModes = EMPTY_MODES,
134
+ style,
135
+ inputStyle,
136
+ inputTextStyle,
137
+ menuStyle,
138
+ accessibilityLabel,
139
+ accessibilityHint,
140
+ onFocus,
141
+ onBlur,
142
+ testID
143
+ }) {
144
+ // ---------------- Modes ----------------
145
+ const {
146
+ modes: globalModes
147
+ } = useTokens();
148
+ const baseModes = useMemo(() => ({
149
+ ...globalModes,
150
+ ...propModes
151
+ }), [globalModes, propModes]);
152
+ const interactive = !isDisabled && !isReadOnly;
153
+
154
+ // ---------------- Query state ----------------
155
+ const isControlledInput = inputValue !== undefined;
156
+ const [internalInput, setInternalInput] = useState(defaultInputValue);
157
+ const query = isControlledInput ? inputValue : internalInput;
158
+
159
+ // ---------------- Selected value state ----------------
160
+ const isControlledValue = value !== undefined;
161
+ const [internalValue, setInternalValue] = useState(defaultValue);
162
+ const currentValue = isControlledValue ? value : internalValue;
163
+
164
+ // ---------------- Open state ----------------
165
+ const isControlledOpen = open !== undefined;
166
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
167
+ const [isFocused, setIsFocused] = useState(false);
168
+ const setOpenState = useCallback(next => {
169
+ if (!isControlledOpen) setInternalOpen(next);
170
+ onOpenChange?.(next);
171
+ }, [isControlledOpen, onOpenChange]);
172
+
173
+ // ---------------- Suggestions ----------------
174
+ const normalizedItems = useMemo(() => (items ?? []).map(normalizeItem), [items]);
175
+ const suggestions = useMemo(() => {
176
+ const trimmed = query.trim();
177
+ if (trimmed.length < minChars) return [];
178
+ const matched = normalizedItems.filter(opt => filter(query, opt));
179
+ return maxResults != null ? matched.slice(0, maxResults) : matched;
180
+ }, [normalizedItems, query, minChars, filter, maxResults]);
181
+ const hasSuggestions = suggestions.length > 0;
182
+ const showEmpty = Boolean(emptyMessage && query.trim().length >= minChars && !hasSuggestions);
183
+ const hasMenuContent = hasSuggestions || showEmpty;
184
+
185
+ // Resolved open state: an explicit `open` prop wins; otherwise the dropdown
186
+ // tracks the internal "wants suggestions" flag (set on focus / typing,
187
+ // cleared on blur / select). Blur and outside-press handle dismissal, so we
188
+ // intentionally do NOT gate on `isFocused` here — that would suppress
189
+ // `defaultOpen` on mount.
190
+ const isOpen = interactive && (isControlledOpen ? Boolean(open) : internalOpen) && hasMenuContent;
191
+
192
+ // ---------------- Token modes (state cascade) ----------------
193
+ const modes = useMemo(() => ({
194
+ ...baseModes,
195
+ 'FormField States': isInvalid ? 'Error' : isReadOnly || isDisabled ? 'Read Only' : isFocused ? 'Active' : baseModes['FormField States'] || 'Idle'
196
+ }), [baseModes, isInvalid, isReadOnly, isDisabled, isFocused]);
197
+ const tokens = useFormFieldTokens(modes);
198
+ const itemTextTokens = useDropdownItemTextTokens(modes);
199
+
200
+ // ---------------- Handlers ----------------
201
+ const inputRef = useRef(null);
202
+ const blurTimer = useRef(null);
203
+ const clearBlurTimer = useCallback(() => {
204
+ if (blurTimer.current) {
205
+ clearTimeout(blurTimer.current);
206
+ blurTimer.current = null;
207
+ }
208
+ }, []);
209
+ useEffect(() => () => clearBlurTimer(), [clearBlurTimer]);
210
+ const setQuery = useCallback(text => {
211
+ if (!isControlledInput) setInternalInput(text);
212
+ onInputChange?.(text);
213
+ }, [isControlledInput, onInputChange]);
214
+ const handleChangeText = useCallback(text => {
215
+ setQuery(text);
216
+ // Typing invalidates a prior selection unless the text still
217
+ // exactly matches the selected option's label.
218
+ if (currentValue != null) {
219
+ const selected = normalizedItems.find(o => o.value === currentValue);
220
+ if (!selected || selected.label !== text) {
221
+ if (!isControlledValue) setInternalValue(null);
222
+ onValueChange?.(null);
223
+ }
224
+ }
225
+ if (!isControlledOpen) setInternalOpen(true);
226
+ }, [setQuery, currentValue, normalizedItems, isControlledValue, onValueChange, isControlledOpen]);
227
+ const handleSelect = useCallback(selectedValue => {
228
+ clearBlurTimer();
229
+ const option = normalizedItems.find(o => o.value === selectedValue);
230
+ if (!option || option.disabled) return;
231
+ setQuery(option.label);
232
+ if (!isControlledValue) setInternalValue(option.value);
233
+ onValueChange?.(option.value, option);
234
+ setOpenState(false);
235
+ inputRef.current?.blur();
236
+ }, [clearBlurTimer, normalizedItems, setQuery, isControlledValue, onValueChange, setOpenState]);
237
+ const handleFocus = useCallback(e => {
238
+ clearBlurTimer();
239
+ setIsFocused(true);
240
+ if (!isControlledOpen) setInternalOpen(true);
241
+ onFocus?.(e);
242
+ }, [clearBlurTimer, isControlledOpen, onFocus]);
243
+ const handleBlur = useCallback(e => {
244
+ // Delay closing so a suggestion press (which blurs the input first
245
+ // on web) still registers before the list unmounts.
246
+ clearBlurTimer();
247
+ blurTimer.current = setTimeout(() => {
248
+ setIsFocused(false);
249
+ if (!isControlledOpen) setInternalOpen(false);
250
+ }, 120);
251
+ onBlur?.(e);
252
+ }, [clearBlurTimer, isControlledOpen, onBlur]);
253
+
254
+ // ---------------- Web outside-press to close ----------------
255
+ const rootRef = useRef(null);
256
+ useEffect(() => {
257
+ if (!IS_WEB || !isOpen) return;
258
+ const handler = e => {
259
+ const node = rootRef.current;
260
+ if (node?.contains && !node.contains(e.target)) {
261
+ clearBlurTimer();
262
+ setIsFocused(false);
263
+ if (!isControlledOpen) setInternalOpen(false);
264
+ }
265
+ };
266
+ document.addEventListener('mousedown', handler);
267
+ return () => document.removeEventListener('mousedown', handler);
268
+ }, [isOpen, isControlledOpen, clearBlurTimer]);
269
+
270
+ // ---------------- Styles ----------------
271
+ const labelTextStyle = {
272
+ color: tokens.labelColor,
273
+ fontFamily: tokens.labelFontFamily,
274
+ fontSize: tokens.labelFontSize,
275
+ lineHeight: tokens.labelLineHeight,
276
+ fontWeight: tokens.labelFontWeight
277
+ };
278
+ const requiredIndicatorStyle = {
279
+ ...labelTextStyle,
280
+ color: '#d93d3d'
281
+ };
282
+ const wrapperStyle = {
283
+ gap: tokens.gap,
284
+ opacity: isDisabled ? 0.5 : 1,
285
+ width: '100%',
286
+ position: 'relative',
287
+ // Keep the dropdown above sibling content when it overflows the field.
288
+ zIndex: isOpen ? 1000 : undefined
289
+ };
290
+ const inputRowStyle = {
291
+ flexDirection: 'row',
292
+ alignItems: 'center',
293
+ backgroundColor: tokens.inputBackground,
294
+ borderColor: tokens.inputBorderColor,
295
+ borderWidth: tokens.inputBorderSize,
296
+ borderStyle: 'solid',
297
+ borderRadius: tokens.inputRadius,
298
+ paddingHorizontal: tokens.inputPaddingH,
299
+ paddingVertical: 0,
300
+ gap: tokens.inputGap,
301
+ minHeight: tokens.inputLineHeight,
302
+ width: '100%'
303
+ };
304
+ const inputTextStyles = {
305
+ flex: 1,
306
+ color: tokens.inputTextColor,
307
+ fontFamily: tokens.inputFontFamily,
308
+ fontSize: tokens.inputFontSize,
309
+ lineHeight: tokens.inputLineHeight,
310
+ fontWeight: tokens.inputFontWeight,
311
+ padding: 0,
312
+ margin: 0,
313
+ ...(IS_WEB ? {
314
+ outlineStyle: 'none',
315
+ outlineWidth: 0,
316
+ outlineColor: 'transparent'
317
+ } : {})
318
+ };
319
+ const placeholderColor = '#888a8d';
320
+ const itemBaseTextStyle = {
321
+ flex: 1,
322
+ color: itemTextTokens.foreground,
323
+ fontFamily: itemTextTokens.fontFamily,
324
+ fontSize: itemTextTokens.fontSize,
325
+ lineHeight: itemTextTokens.lineHeight,
326
+ fontWeight: '400'
327
+ };
328
+
329
+ // ---------------- Support text ----------------
330
+ const supportStatus = isInvalid ? 'Error' : 'Neutral';
331
+ const supportLabel = isInvalid && errorMessage ? errorMessage : supportText;
332
+
333
+ // ---------------- Accessibility ----------------
334
+ const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Search';
335
+ const a11yProps = {
336
+ accessibilityRole: IS_WEB ? undefined : 'search',
337
+ accessibilityLabel: resolvedA11yLabel,
338
+ accessibilityState: {
339
+ disabled: isDisabled,
340
+ expanded: isOpen
341
+ }
342
+ };
343
+ if (accessibilityHint) a11yProps.accessibilityHint = accessibilityHint;
344
+
345
+ // ---------------- Suggestion highlight ----------------
346
+ const renderHighlighted = useCallback(optLabel => {
347
+ const trimmed = query.trim();
348
+ if (!highlightMatch || trimmed.length === 0) {
349
+ return /*#__PURE__*/_jsx(Text, {
350
+ style: itemBaseTextStyle,
351
+ children: optLabel
352
+ });
353
+ }
354
+ const idx = optLabel.toLowerCase().indexOf(trimmed.toLowerCase());
355
+ if (idx < 0) {
356
+ return /*#__PURE__*/_jsx(Text, {
357
+ style: itemBaseTextStyle,
358
+ children: optLabel
359
+ });
360
+ }
361
+ const before = optLabel.slice(0, idx);
362
+ const match = optLabel.slice(idx, idx + trimmed.length);
363
+ const after = optLabel.slice(idx + trimmed.length);
364
+ return /*#__PURE__*/_jsxs(Text, {
365
+ style: itemBaseTextStyle,
366
+ children: [before, /*#__PURE__*/_jsx(Text, {
367
+ style: {
368
+ fontWeight: '700'
369
+ },
370
+ children: match
371
+ }), after]
372
+ });
373
+ }, [query, highlightMatch, itemBaseTextStyle]);
374
+
375
+ // ---------------- Render ----------------
376
+ return /*#__PURE__*/_jsxs(View, {
377
+ ref: rootRef,
378
+ style: [wrapperStyle, style],
379
+ pointerEvents: isDisabled ? 'none' : 'auto',
380
+ testID: testID,
381
+ children: [label != null && /*#__PURE__*/_jsxs(View, {
382
+ style: styles.labelRow,
383
+ children: [/*#__PURE__*/_jsx(Text, {
384
+ style: labelTextStyle,
385
+ children: label
386
+ }), isRequired && /*#__PURE__*/_jsx(Text, {
387
+ style: requiredIndicatorStyle,
388
+ children: " *"
389
+ })]
390
+ }), /*#__PURE__*/_jsxs(View, {
391
+ style: styles.anchor,
392
+ children: [/*#__PURE__*/_jsx(Pressable, {
393
+ style: [inputRowStyle, inputStyle],
394
+ onPress: () => inputRef.current?.focus(),
395
+ accessible: false,
396
+ children: /*#__PURE__*/_jsx(RNTextInput, {
397
+ ref: inputRef,
398
+ style: [inputTextStyles, inputTextStyle],
399
+ value: query,
400
+ onChangeText: handleChangeText,
401
+ onFocus: handleFocus,
402
+ onBlur: handleBlur,
403
+ placeholder: placeholder,
404
+ placeholderTextColor: placeholderColor,
405
+ editable: interactive,
406
+ autoCapitalize: "none",
407
+ autoComplete: "off",
408
+ autoCorrect: false,
409
+ ...a11yProps,
410
+ ...(IS_WEB ? {
411
+ accessibilityRole: 'search',
412
+ 'aria-autocomplete': 'list',
413
+ 'aria-expanded': isOpen
414
+ } : {})
415
+ })
416
+ }), isOpen && /*#__PURE__*/_jsx(View, {
417
+ style: [styles.popup, {
418
+ top: '100%',
419
+ marginTop: menuOffset
420
+ }]
421
+ // Keep taps from dismissing the keyboard before the
422
+ // item's press handler runs on native.
423
+ ,
424
+ children: /*#__PURE__*/_jsx(Dropdown, {
425
+ modes: modes,
426
+ maxHeight: menuMaxHeight,
427
+ style: menuStyle,
428
+ accessibilityLabel: `${resolvedA11yLabel} suggestions`,
429
+ children: hasSuggestions ? suggestions.map(opt => {
430
+ const isSelected = opt.value === currentValue;
431
+ return /*#__PURE__*/_jsx(DropdownItem, {
432
+ value: opt.value,
433
+ selected: isSelected,
434
+ disabled: opt.disabled ?? false,
435
+ onPress: handleSelect,
436
+ modes: modes,
437
+ children: renderItem ? renderItem(opt, {
438
+ query,
439
+ isSelected
440
+ }) : renderHighlighted(opt.label)
441
+ }, `sg-${opt.value}`);
442
+ }) : showEmpty && /*#__PURE__*/_jsx(View, {
443
+ style: styles.emptyRow,
444
+ children: /*#__PURE__*/_jsx(Text, {
445
+ style: [itemBaseTextStyle, {
446
+ color: placeholderColor
447
+ }],
448
+ children: emptyMessage
449
+ })
450
+ })
451
+ })
452
+ })]
453
+ }), supportLabel != null && supportLabel !== '' && /*#__PURE__*/_jsx(SupportText, {
454
+ label: supportLabel,
455
+ status: supportStatus,
456
+ modes: modes
457
+ })]
458
+ });
459
+ }
460
+ const styles = {
461
+ labelRow: {
462
+ flexDirection: 'row',
463
+ alignItems: 'baseline'
464
+ },
465
+ anchor: {
466
+ position: 'relative',
467
+ width: '100%',
468
+ zIndex: 1
469
+ },
470
+ popup: {
471
+ position: 'absolute',
472
+ left: 0,
473
+ right: 0,
474
+ zIndex: 1000
475
+ },
476
+ emptyRow: {
477
+ paddingHorizontal: 12,
478
+ paddingVertical: 12
479
+ }
480
+ };
481
+ export default SuggestiveSearch;
@@ -4,6 +4,8 @@ import React from 'react';
4
4
  import { Text as RNText } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import Skeleton from '../../skeleton/Skeleton';
8
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
7
9
  import { jsx as _jsx } from "react/jsx-runtime";
8
10
  const TEXT_ALIGN_MAP = {
9
11
  Left: 'left',
@@ -15,7 +17,10 @@ function Text({
15
17
  textAlign = 'Left',
16
18
  modes = EMPTY_MODES,
17
19
  style,
18
- numberOfLines
20
+ numberOfLines,
21
+ loading,
22
+ skeletonWidth,
23
+ skeletonLines
19
24
  }) {
20
25
  const foreground = getVariableByName('text/foreground', modes) ?? '#000000';
21
26
  const fontFamily = getVariableByName('text/fontFamily', modes) ?? 'JioType';
@@ -23,6 +28,31 @@ function Text({
23
28
  const fontWeight = getVariableByName('text/fontWeight', modes) ?? '500';
24
29
  const lineHeight = getVariableByName('text/lineHeight', modes) ?? 20;
25
30
  const letterSpacing = getVariableByName('text/letterSpacing', modes) ?? -0.5;
31
+
32
+ // Skeleton short-circuit. The hook is always called (stable order); the
33
+ // surrounding group's `active` flag is the default, but an explicit
34
+ // `loading` prop overrides both directions.
35
+ const {
36
+ active: groupActive
37
+ } = useSkeleton();
38
+ const isLoading = loading ?? groupActive;
39
+ if (isLoading) {
40
+ const resolvedFontSize = fontSize;
41
+ const resolvedLineHeight = lineHeight;
42
+ const lines = Math.max(1, skeletonLines ?? numberOfLines ?? 1);
43
+ // Heuristic width based on the actual content length so the placeholder
44
+ // visually matches what's about to load. Capped to a reasonable lower
45
+ // bound so very short labels still produce a visible block.
46
+ const contentLength = typeof children === 'string' ? children.length : typeof text === 'string' ? text.length : 12;
47
+ const charWidth = resolvedFontSize * 0.55;
48
+ const computedWidth = Math.max(charWidth * 4, contentLength * charWidth);
49
+ return /*#__PURE__*/_jsx(Skeleton, {
50
+ kind: "text",
51
+ width: skeletonWidth ?? computedWidth,
52
+ height: resolvedLineHeight * lines,
53
+ modes: modes
54
+ });
55
+ }
26
56
  const textStyle = {
27
57
  color: foreground,
28
58
  fontFamily: fontFamily,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useRef, useState } from 'react';
4
4
  import { Pressable, View, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import Icon from '../../icons/Icon';
@@ -114,6 +114,11 @@ function TextInput({
114
114
  // Track focus state to hide placeholder when focused
115
115
  const [isFocused, setIsFocused] = useState(false);
116
116
  const [isHovered, setIsHovered] = useState(false);
117
+ // Ref to the underlying native input so a tap anywhere inside the Pressable
118
+ // wrapper can programmatically focus it. Without this, on Android the
119
+ // wrapping Pressable becomes the touch responder on the first tap and the
120
+ // native input only gains focus on the *second* tap.
121
+ const inputRef = useRef(null);
117
122
 
118
123
  // Resolve container tokens
119
124
  const backgroundColor = getVariableByName('textInput/background', modes) || '#f5f5f5';
@@ -207,12 +212,22 @@ function TextInput({
207
212
  return /*#__PURE__*/_jsxs(Pressable, {
208
213
  style: [containerStyle, focusContainerStyle, hoverStyle, style],
209
214
  onHoverIn: () => setIsHovered(true),
210
- onHoverOut: () => setIsHovered(false),
215
+ onHoverOut: () => setIsHovered(false)
216
+ // Forward taps on the wrapper (padding, leading icon gutter, etc.) to the
217
+ // native input. This guarantees the keyboard opens on the FIRST tap on
218
+ // Android instead of requiring a second tap.
219
+ ,
220
+ onPress: () => inputRef.current?.focus()
221
+ // The native input is the real accessible element; don't add a redundant
222
+ // focusable node for screen readers.
223
+ ,
224
+ accessible: false,
211
225
  children: [processedLeading && /*#__PURE__*/_jsx(View, {
212
226
  accessibilityElementsHidden: true,
213
227
  importantForAccessibility: "no",
214
228
  children: processedLeading
215
229
  }), /*#__PURE__*/_jsx(RNTextInput, {
230
+ ref: inputRef,
216
231
  accessibilityLabel: undefined,
217
232
  accessibilityHint: accessibilityHint,
218
233
  placeholder: displayPlaceholder,
@@ -35,13 +35,21 @@ function Title({
35
35
  const gap = getVariableByName('title/gap', modes) || 8;
36
36
  const labelColor = getVariableByName('title/label/color', modes) || '#0d0d0f';
37
37
  const fontSize = getVariableByName('title/fontSize', modes) || 26;
38
- const lineHeight = getVariableByName('title/lineHeight', modes) || 26;
38
+ const rawLineHeight = getVariableByName('title/lineHeight', modes) || 26;
39
+ // The Figma title tokens ship with `lineHeight === fontSize` (26/26). On
40
+ // native (especially Android) that produces a Text box exactly `lineHeight`
41
+ // tall, which clips descenders like p/g/y/q/j at the bottom — particularly
42
+ // noticeable with `numberOfLines={1}`. Clamp to ~1.2x fontSize so descenders
43
+ // always fit. When a downstream token already provides adequate leading,
44
+ // the original value is preserved.
45
+ const lineHeight = Math.max(rawLineHeight, Math.ceil(fontSize * 1.2));
39
46
  const fontFamily = getVariableByName('title/fontFamily', modes) || 'System';
40
47
  const fontWeightRaw = getVariableByName('title/fontWeight', modes) || 900;
41
48
  const fontWeight = typeof fontWeightRaw === 'number' ? fontWeightRaw.toString() : fontWeightRaw;
42
49
  const subtitleColor = getVariableByName('pageSubtitle/label/color', modes) || '#0d0d0f';
43
50
  const subtitleFontSize = getVariableByName('pageSubtitle/fontSize', modes) || 14;
44
- const subtitleLineHeight = getVariableByName('pageSubtitle/lineHeight', modes) || 18;
51
+ const subtitleRawLineHeight = getVariableByName('pageSubtitle/lineHeight', modes) || 18;
52
+ const subtitleLineHeight = Math.max(subtitleRawLineHeight, Math.ceil(subtitleFontSize * 1.2));
45
53
  const subtitleFontFamily = getVariableByName('pageSubtitle/fontFamily', modes) || 'System';
46
54
  const subtitleFontWeightRaw = getVariableByName('pageSubtitle/fontWeight', modes) || 500;
47
55
  const subtitleFontWeight = typeof subtitleFontWeightRaw === 'number' ? subtitleFontWeightRaw.toString() : subtitleFontWeightRaw;
@@ -25,9 +25,11 @@ export { default as Divider } from './Divider/Divider';
25
25
  export { default as Drawer } from './Drawer/Drawer';
26
26
  export { default as Dropdown, DropdownItem } from './Dropdown/Dropdown';
27
27
  export { default as DropdownInput } from './DropdownInput/DropdownInput';
28
+ export { default as SuggestiveSearch } from './SuggestiveSearch/SuggestiveSearch';
28
29
  export { default as CardCTA } from './CardCTA/CardCTA';
29
30
  export { default as DebitCard } from './DebitCard/DebitCard';
30
31
  export { default as FilterBar } from './FilterBar/FilterBar';
32
+ export { default as FullscreenModal } from './FullscreenModal/FullscreenModal';
31
33
  export { default as Form } from './Form/Form';
32
34
  export { useFormContext } from './Form/Form';
33
35
  export { default as FormField } from './FormField/FormField';
@@ -48,9 +50,11 @@ export { default as LinearMeter } from './LinearMeter/LinearMeter';
48
50
  export { default as LinearProgress } from './LinearProgress/LinearProgress';
49
51
  export { default as ListGroup } from './ListGroup/ListGroup';
50
52
  export { default as LottieIntroBlock } from './LottieIntroBlock/LottieIntroBlock';
53
+ export { default as LottiePlayer } from './LottiePlayer/LottiePlayer';
51
54
  export { default as ListItem } from './ListItem/ListItem';
52
55
  export { default as MediaCard } from './MediaCard/MediaCard';
53
56
  export { default as MerchantProfile } from './MerchantProfile/MerchantProfile';
57
+ export { default as MessageField } from './MessageField/MessageField';
54
58
  export { default as MetricLegendItem } from './MetricLegendItem/MetricLegendItem';
55
59
  export { default as MoneyValue } from './MoneyValue/MoneyValue';
56
60
  export { default as NoteInput } from './NoteInput/NoteInput';
@@ -74,6 +78,7 @@ export { default as UpiHandle } from './UpiHandle/UpiHandle';
74
78
  export { default as VStack } from './VStack/VStack';
75
79
  export { default as ChipGroup } from './ChipGroup/ChipGroup';
76
80
  export { default as EmptyState } from './EmptyState/EmptyState';
81
+ export { default as ExpandableCheckbox } from './ExpandableCheckbox/ExpandableCheckbox';
77
82
  export { default as Accordion } from './Accordion/Accordion';
78
83
  export { default as AccordionCheckbox } from './AccordionCheckbox/AccordionCheckbox';
79
84
  export { default as ActionTile } from './ActionTile/ActionTile';