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